From cbcf651366473c387468d1703b70ed3e05b55f7d Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 13:10:42 -0700 Subject: [PATCH 01/25] feat(desktop): add macOS app vertical slice --- .gitignore | 4 + apps/desktop/index.html | 13 + apps/desktop/package.json | 43 + apps/desktop/scripts/core-sidecar.mjs | 93 + apps/desktop/scripts/stage-sidecar.mjs | 94 + apps/desktop/src-tauri/Cargo.lock | 4688 +++++++++++++++++ apps/desktop/src-tauri/Cargo.toml | 33 + apps/desktop/src-tauri/Entitlements.plist | 10 + apps/desktop/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 7 + apps/desktop/src-tauri/icons/icon.icns | Bin 0 -> 378171 bytes apps/desktop/src-tauri/icons/icon.png | Bin 0 -> 257096 bytes apps/desktop/src-tauri/rust-toolchain.toml | 5 + apps/desktop/src-tauri/src/lib.rs | 421 ++ apps/desktop/src-tauri/src/main.rs | 3 + apps/desktop/src-tauri/tauri.conf.json | 46 + apps/desktop/src/App.tsx | 212 + apps/desktop/src/bridge.ts | 57 + apps/desktop/src/components/CodeEditor.tsx | 103 + apps/desktop/src/components/FileTree.tsx | 53 + .../src/components/GraphPanel.test.tsx | 74 + apps/desktop/src/components/GraphPanel.tsx | 289 + apps/desktop/src/main.tsx | 7 + apps/desktop/src/model.test.ts | 65 + apps/desktop/src/model.ts | 195 + apps/desktop/src/packaging.test.ts | 23 + apps/desktop/src/styles.css | 147 + apps/desktop/tsconfig.json | 22 + apps/desktop/vite.config.ts | 16 + apps/desktop/vitest.config.ts | 8 + docs/research/desktop-app-stack.md | 100 + packages/core/src/index.ts | 6 + packages/core/src/workspace/requestGraph.ts | 99 + .../core/tests/workspace/requestGraph.test.ts | 50 + pnpm-lock.yaml | 318 +- 35 files changed, 7306 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/index.html create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/scripts/core-sidecar.mjs create mode 100644 apps/desktop/scripts/stage-sidecar.mjs create mode 100644 apps/desktop/src-tauri/Cargo.lock create mode 100644 apps/desktop/src-tauri/Cargo.toml create mode 100644 apps/desktop/src-tauri/Entitlements.plist create mode 100644 apps/desktop/src-tauri/build.rs create mode 100644 apps/desktop/src-tauri/capabilities/default.json create mode 100644 apps/desktop/src-tauri/icons/icon.icns create mode 100644 apps/desktop/src-tauri/icons/icon.png create mode 100644 apps/desktop/src-tauri/rust-toolchain.toml create mode 100644 apps/desktop/src-tauri/src/lib.rs create mode 100644 apps/desktop/src-tauri/src/main.rs create mode 100644 apps/desktop/src-tauri/tauri.conf.json create mode 100644 apps/desktop/src/App.tsx create mode 100644 apps/desktop/src/bridge.ts create mode 100644 apps/desktop/src/components/CodeEditor.tsx create mode 100644 apps/desktop/src/components/FileTree.tsx create mode 100644 apps/desktop/src/components/GraphPanel.test.tsx create mode 100644 apps/desktop/src/components/GraphPanel.tsx create mode 100644 apps/desktop/src/main.tsx create mode 100644 apps/desktop/src/model.test.ts create mode 100644 apps/desktop/src/model.ts create mode 100644 apps/desktop/src/packaging.test.ts create mode 100644 apps/desktop/src/styles.css create mode 100644 apps/desktop/tsconfig.json create mode 100644 apps/desktop/vite.config.ts create mode 100644 apps/desktop/vitest.config.ts create mode 100644 docs/research/desktop-app-stack.md create mode 100644 packages/core/src/workspace/requestGraph.ts create mode 100644 packages/core/tests/workspace/requestGraph.test.ts diff --git a/.gitignore b/.gitignore index e533556be..46217296d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ dist/ .DS_Store dist-e2e/ /artifacts/ +apps/desktop/src-tauri/binaries/* +apps/desktop/src-tauri/gen/ +apps/desktop/src-tauri/runtime/ +apps/desktop/src-tauri/target/ .worktrees/ .playwright-mcp/ .playwright-cli/ diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 000000000..7038a60ad --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + CodeGraphy + + +
+ + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 000000000..b406d02e0 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,43 @@ +{ + "name": "@codegraphy-dev/desktop", + "version": "0.1.0", + "private": true, + "description": "CodeGraphy desktop app for macOS", + "type": "module", + "engines": { + "node": "^22.14.0 || >=23.6.0" + }, + "scripts": { + "build": "vite build", + "build:bundle": "tauri build", + "build:sidecar": "node scripts/stage-sidecar.mjs", + "dev": "tauri dev --no-watch", + "lint": "eslint src scripts vite.config.ts vitest.config.ts", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/language": "^6.12.4", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@codegraphy-dev/core": "workspace:*", + "@codegraphy-dev/graph-renderer": "workspace:*", + "@lezer/highlight": "^1.2.3", + "@tauri-apps/api": "^2.11.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.4", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "jsdom": "^25.0.0", + "typescript": "^5.3.3", + "vite": "^6.2.0", + "vitest": "^3.0.0" + } +} diff --git a/apps/desktop/scripts/core-sidecar.mjs b/apps/desktop/scripts/core-sidecar.mjs new file mode 100644 index 000000000..6d3eccce8 --- /dev/null +++ b/apps/desktop/scripts/core-sidecar.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { createInterface } from 'node:readline'; + +const coreModuleUrl = process.env.CODEGRAPHY_DESKTOP_CORE_MODULE + ? new URL(`file://${process.env.CODEGRAPHY_DESKTOP_CORE_MODULE}`) + : new URL('./core/dist/index.js', import.meta.url); +const core = await import(coreModuleUrl.href); + +function write(message) { + process.stdout.write(`${JSON.stringify(message)}\n`); +} + +function isObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function parseRequest(value) { + if (!isObject(value) || value.kind !== 'request' || typeof value.id !== 'number') { + throw new Error('Core request must have kind "request" and a numeric id.'); + } + if ((value.method !== 'open' && value.method !== 'index') || !isObject(value.params)) { + throw new Error('Core request method must be "open" or "index".'); + } + if (typeof value.params.workspaceRoot !== 'string' || value.params.workspaceRoot.length === 0) { + throw new Error('Core request requires params.workspaceRoot.'); + } + return { + id: value.id, + includeSymbols: value.params.includeSymbols === true, + method: value.method, + workspaceRoot: value.params.workspaceRoot, + }; +} + +function graphRequest(request) { + return { + workspacePath: request.workspaceRoot, + ...(request.includeSymbols + ? { projection: { nodeTypes: ['file', 'folder', 'symbol'] } } + : {}), + }; +} + +function indexResult(result, graph) { + if (graph.kind !== 'ready') { + throw new Error('Core did not return the Graph Cache after Indexing.'); + } + return { + ...graph, + indexing: result.indexing, + discovery: { + indexedFiles: result.files.length, + totalFound: result.totalFound, + limitReached: result.limitReached, + }, + }; +} + +async function runRequest(request) { + if (request.method === 'open') { + const cached = core.requestCodeGraphyWorkspaceGraph(graphRequest(request)); + if (cached.kind === 'ready' || cached.kind === 'unreadable') return cached; + } + + write({ + kind: 'event', + event: 'indexing', + workspaceRoot: request.workspaceRoot, + }); + const result = await core.indexCodeGraphyWorkspace({ + workspaceRoot: request.workspaceRoot, + }); + return indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); +} + +const lines = createInterface({ input: process.stdin, crlfDelay: Infinity }); +for await (const line of lines) { + let requestId = null; + try { + const request = parseRequest(JSON.parse(line)); + requestId = request.id; + const result = await runRequest(request); + write({ kind: 'response', id: request.id, outcome: 'success', result }); + } catch (error) { + write({ + kind: 'response', + id: requestId, + outcome: 'error', + error: error instanceof Error ? error.message : String(error), + }); + } +} diff --git a/apps/desktop/scripts/stage-sidecar.mjs b/apps/desktop/scripts/stage-sidecar.mjs new file mode 100644 index 000000000..d32e51436 --- /dev/null +++ b/apps/desktop/scripts/stage-sidecar.mjs @@ -0,0 +1,94 @@ +import { execFileSync } from 'node:child_process'; +import { + chmodSync, + copyFileSync, + cpSync, + existsSync, + mkdirSync, + rmSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = path.resolve(appRoot, '../..'); +const runtimeRoot = path.join(appRoot, 'src-tauri', 'runtime'); +const binaryRoot = path.join(appRoot, 'src-tauri', 'binaries'); +const nodeVersion = '22.23.2'; +const hostTargetByArchitecture = { + arm64: 'aarch64-apple-darwin', + x64: 'x86_64-apple-darwin', +}; +const nodeDistributionArchitecture = { + arm64: 'arm64', + x64: 'x64', +}; + +if (process.platform !== 'darwin') { + throw new Error('CodeGraphy desktop sidecars must be staged on macOS.'); +} + +const hostTarget = hostTargetByArchitecture[process.arch]; +if (!hostTarget) throw new Error(`Unsupported macOS architecture: ${process.arch}`); +const target = process.env.CODEGRAPHY_DESKTOP_TARGET ?? hostTarget; +if (target !== hostTarget) { + throw new Error(`Cannot stage ${hostTarget} native Core modules for ${target}.`); +} + +const runtimeCache = process.env.CODEGRAPHY_DESKTOP_RUNTIME_CACHE + ?? path.join(os.homedir(), 'Library', 'Caches', 'CodeGraphy', 'desktop-runtime'); +const nodeArchiveName = `node-v${nodeVersion}-darwin-${nodeDistributionArchitecture[process.arch]}.tar.gz`; +const nodeDistributionRoot = path.join(runtimeCache, nodeArchiveName.replace(/\.tar\.gz$/u, '')); +const nodeExecutable = path.join(nodeDistributionRoot, 'bin', 'node'); +if (!existsSync(nodeExecutable)) { + mkdirSync(runtimeCache, { recursive: true }); + const archivePath = path.join(runtimeCache, nodeArchiveName); + execFileSync('curl', [ + '--fail', + '--location', + '--output', + archivePath, + `https://nodejs.org/dist/v${nodeVersion}/${nodeArchiveName}`, + ], { stdio: 'inherit' }); + execFileSync('tar', ['-xzf', archivePath, '-C', runtimeCache], { stdio: 'inherit' }); +} +execFileSync(nodeExecutable, ['--version'], { stdio: 'inherit' }); + +execFileSync('pnpm', [ + '-w', + 'exec', + 'turbo', + 'run', + 'build', + '--filter=@codegraphy-dev/core...', +], { cwd: repoRoot, stdio: 'inherit' }); + +rmSync(runtimeRoot, { force: true, recursive: true }); +mkdirSync(runtimeRoot, { recursive: true }); +execFileSync('pnpm', [ + '--config.node-linker=hoisted', + '--filter', + '@codegraphy-dev/core', + 'deploy', + '--legacy', + '--prod', + path.join(runtimeRoot, 'core'), +], { cwd: repoRoot, stdio: 'inherit' }); +// Hoisted deploy can rewrite shared workspace linker metadata. Restore the +// checked-in lockfile layout before any repository build or test continues. +execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot, stdio: 'inherit' }); +copyFileSync(path.join(appRoot, 'scripts', 'core-sidecar.mjs'), path.join(runtimeRoot, 'sidecar.mjs')); +execFileSync(nodeExecutable, [ + '--input-type=module', + '--eval', + `await import(${JSON.stringify(pathToFileURL(path.join(runtimeRoot, 'core', 'dist', 'index.js')).href)})`, +], { stdio: 'inherit' }); + +mkdirSync(binaryRoot, { recursive: true }); +const sidecarPath = path.join(binaryRoot, `codegraphy-core-${target}`); +copyFileSync(nodeExecutable, sidecarPath); +chmodSync(sidecarPath, 0o755); + +const licensesSource = path.join(repoRoot, 'LICENSE'); +cpSync(licensesSource, path.join(runtimeRoot, 'LICENSE')); diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 000000000..f8204dd52 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4688 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "codegraphy-desktop" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-shell", + "tempfile", + "tokio", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.19", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.19", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.19", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.19", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 000000000..b9f154845 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "codegraphy-desktop" +version = "0.1.0" +description = "CodeGraphy desktop app for macOS" +authors = ["Joe Soboleski"] +edition = "2024" +license = "MIT" +repository = "https://github.com/joesobo/CodeGraphyV4" +rust-version = "1.85" + +[lib] +name = "codegraphy_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.4", features = [] } + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +tauri = { version = "2.11.5", features = [] } +tauri-plugin-dialog = "2.7.2" +tauri-plugin-shell = "2.3.5" +tempfile = "3.23" +tokio = { version = "1.48", features = ["io-util", "process", "sync"] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/apps/desktop/src-tauri/Entitlements.plist b/apps/desktop/src-tauri/Entitlements.plist new file mode 100644 index 000000000..9ab52e13b --- /dev/null +++ b/apps/desktop/src-tauri/Entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 000000000..d860e1e6a --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 000000000..c3ce9af64 --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop", + "description": "CodeGraphy desktop window capabilities", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..c02930c7ef5a11908566acf56398ddd7dc85491b GIT binary patch literal 378171 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

4jt60%o4G7(>hFhB& zzB~BV2V_gAaCoeb&|LFA_{egasPIm)vw#f?sQ={oVjs&?CSY5pV&Qz3303C1TWgYl zvA<%nvR2O>ID9CvzjNLk(Q&h5s>7o0aCw>2%X@lJ#BV}dAF!Nr0(X5}PP@#%esEXj ztH2i1h{TEH<;p5~stkmVJ32$B%9zNny^W4m<=0H`oJMPQOwY)mz{CAupMMq5M9Y3( zk2Htf^FBnCRB#7;UPfi?K?i$PC{*(X+?M5|JZW}BULiwqfdTjJOEhcD(`O$_2k^=) zpZSy>rgo#j=bCn33U+P#F7}AHtLRccg`t@F2Ya3%XZ;xbAKwk(B5(+K#$NI5NiQ1_ z?P!bWN?)#mpe4DB1TE7wG}j-W8Vzfa^Qf`+V^6=Fx;B8e<285uuap*tRM;&}M=ZDurpq9@ zLYvnzIu{4ufEg=21J|X&!I>V+46rj`N8m&v;Gq0{_iU)~dfW2`t?KSr{afr?yI30I z#m6@S_8d<_W|8?gJ;yNeua--d3O@8X!tw)brOcLY46l8r*LOmOziqSniNyL7LVCUe~(q*-5e z_n`jbE20&j96kDSyz+x{cSHN$!v4n6J~mSQ7Gjzn>(O3Ro5w@MkjVa$9$Jyx{W*d; zC)t`K^UpF}6}}Qut_wtZcEr!o6>m+_>7eNenOWwtNL_c{!3Lrt@(8TsRHYG}1)9Li zC--7MVe@cVU@bF^bsc}Dgak~5e}|SRH?D5do|#FhFU8|XD{{*BfuFqHh|KSGPn&!= zRU?3JgiHj4x8-7jgOmBe1@8?%dFJ?@mzXA$`<~R|V++zf4T3l6`vvge!*a(P1N(1ne$g>=_D5bH$hGJI>goWOP?^CZ(BOC}lWe=Gxl;pUUb zJ2L_h8UTI@AQO>-3+wKsuEwzLwYRrkrvS@s%?nEdQ|7p*p%w9**|S0{fCT{hRK!sL zBZII=)ud!t5zq~GK;HmRIslb2s|x6{Yw4fbwbeb(HJY_j1E3s0e`(v%%1zAs*zX#< z4tdsRt=hz7o3OqzF)08je}c~j2lEYpyHz?=kW`k;|5JI+MRT8dXu`u*zJ7Y3+(O&# zaD2n0si{F~Nk^RhV{}?M<3&msUFZQAkf1=*lGsn(=%cMN@%#WOyLM7$b4}&i>OJ%9 zt&-L#fEoZ3ru-d46$vYBhrcxvhW)2`ADxtzYN5wHh*0yX(`V&0M#y*<4G1s?{!1Ay zeFFX-df;BP_Pq~X@7nUff8`676wd%?GMy3Y5y4=Jz_cNbY639@;E0l-^t-#SnLz@g zJfls+04n8S2>9(er`dGC8m_gLXw5LLsu7?DfJ`)K1rw3s{+F!~{l|gBW%?!I9nCu7uWN!hsx}65@f3p4M8vx~M1O#3Lpiverm?sP8&yz*~ z8R?z7a=^PcVnPsri>Iq#dBo23Pj0Kd%6&$*Yg z0YC$WC4n<%&X77p0N6LcfdQjPH;|T@*Pk5<*(s5O28LPyDrLHexOLPC*VtFM=5p=j z8f*!IlpjD1fN=$>IM06p#&<9g|5Wl(|C1Y~THtU~Ij2p++gCC{bP)h!0EC?P^rAU@ zj6pfqw)=-hielmz*r0z!QTjp2lkh=SVVcr8=hX(4cg+m-FT?Vs z?9LP9DgpwHoT^lw0Z?n#-fP%i#P+mZmlH4wlA6oX2tcWw+65RS{CPsc#y=x=(&=>_ zvl)#`LPxJPoq<8+IRYx+;o;G;&;1t@g7ZkvORfaw>0{aIg-oP98rWx`hHa5SIuyjz z08ptIot3d`ZJS+tU$ASjRt2a5P+G2kB2MjlEJI-q>fQq9O|8znoPR#}%z(`ds4d#( z)YZ3(1-A_v1v+HH+d≈&P@;X4&7>Zcy}Mkgd<810rmiKTN1L zjF&K8mi%Tv8u}lT+$MKfSl7;Wx1{N1ojdyhL{Wm~Jma{HF!Z6{z`=uu<=~;ChULyD zA}U84pm5?0L+O*BKQlCd{s}1)Es0JkyY|Cr6ImeHP+MC_I5ZYBp^8d{qynaKZMJK4 zn^zX7SOC#7z&kDbi+iL$^YHq2vHhwwp3-)!2EZ*)+_FtCd);&N^$ke>z<{yg9bNC; zc(~aV0KSXuZF~Fr;6d(KkI z5U2*g-7@~&FCoyRQ8e^_M;!MJFaYMxoh9?;%r?=06*H2W;iV`x4~n%=-gS~4LfqqU_adtK5IVlq@z zCNLGiHF`DIY-I>k13>P!><LoSm%DnaNm~!{jDp^3MBz>}b&avKbRb#|?O(XQQE7AT4VxLxJtEA}~@Oc+nu*CVR@Z!ZNv z?~=-F`t)fsW5zVo=iYhjL8Kw|Ft<(Q{Dq5`WWyU9jX|*Hk%vqLf-wM)dWgr$Ab3M% z>=`1bPM?({h*q3I{eLa0BpMqVA}amM2TK~(KL9crg{iNrEg%R|9sre!W(XX$41rv$ z0Wy;JFwVF&U5#9g0LU$t{f*c~lL^vQ5|;jn2RPB0bUU88?Kj^9rN*#x65c;i?%W2k zSfptH0_SLK41o^#+Qsv-`_sMh@s3@lI)FmsgJ}XrASPl#phr)TA1Qw8Ef(T~&0FR5 z4eyxJKVlWgnGh}bFQiE3Z!r}!d(JGRV`dv8<2tHl6&0!2vOqsy#x;ADUBmf2jc8e* zMgVT8{H^DovPJ;Q{+`6^b$Gu`G?a8lipP28PFh-;47DwbD>4QH5PfK!%DoO=?ufJ) zAmDFad&?-k&wuWD!+a-(a(5Kb0*2XxanJjYjEtB_1%*og@BZog^0QyOW-1t3QJUvk z^+p5!3mg6r2SEd%1Cfx`58fw_JoJF60LZl=)^$-uswQ>INC2Lnb=Y3O_I*4DJuVVJ zL?G9AH2_qKO`w!jB<%M03?|G|7Fgn!{b}^%c4%%r-O}9DWUv4Y?NcQLq+9S=L}3+Y z0MNT`0m~f~-$&ba7{xyiUhku8R*S1jaM1}WzfyzW!d!mZlqx0?jaky4x8HqV-r4wp zoIi0$;*A~B+1ZXpMk7X9M_D0}Lf(HAMg=1<58Qtb#wr+N%>`7=Dk@S95pAy3ja<92 z9$dlLhu1yc8m}cUs>E)(Xp-}PEq0el2>wfIx4%291c-UcrK6(_O86mJ3q{QR;o;#> z*_pPq(bm`V3t3Wk_Uw7tv17Nq_4X#D1oq{9mfkoGhEfCmsDOt&YGS4&zW>9Y%MX6^ z3%PXZvc#bL3H)PZA`>wTWhPV1t{<0#mjQ;`~@h#D+aVz zhqAYnL!w3Kbdi4R3CgpZ@&UvK@vAKQ?{p6d8swVn~1@PQ&n=X=G&vLmb4^DP7Xl)oCLjDeVQI zB30ZikZbt8cpmrTS-5~PQn2Q$0ibpNS!O@JHlcq^nDv+}9R=2Yh8+O1QyFLX!z#ssE!^ZPp^YDXtkpRa>SjSY3u z)!8mSKrfiS+8l;=5AZeAPE1IxsLn586Hm9m*CW?v+~ZnAIJ}X{7cmP?>0sU zBOg4@QdA{`x&M^8{_OhK1{TKU%a(xuzfIf!Rt$zF6B!%lBQD34KNiU@2<`csRcMM9OM5#khL*=tgYa}Uw~Bc`H&6wFUQ^_aZ$;oZJWR{^VH4%sM1;$W&}7a0Ld=q;@ls!?Sc=Ewf3t4Ae#HfW@SNN zY0dqYpf>=Cd5A_Q0nV}GC$pXVm?w{#0LP^Q7;)e}X@bvRxF|=C9+#_EdnADf0qJ*V zfT)O`a%V?JmWl69BM;2;)5u`R|0LS`zWL53`Ngj`$feWA!Smk^1E|X==2Gzy@LKbn z2|P@q+MssfJb7&G!?Ny)M@l;#64zj_ zVgb|uD3<&0v~{EF3MeEs_b*X_0dtv_X@~&4{L-^V0saJFxP##B&w>FX&WDGdkYtrD z2crS>m)I-dAO87=@)iJsOBOAZ`vDGQg~GC>i?W;nw=3tN_&?t9iM;XFyRvuR0h4N= zAwVN1*4SnY5tjOuih?&(Bw-{Xyq1H;u$n(_u1x9b$fjfXo+|%|Q~Y7nD{UHBkrk5v zaZVPCBHe8&EF8JtAnqn+@j1THQN>sQx4vJ%wRi_>(Ur-L1xTnznxFf}@N@qeuhE3A zk`aZZo$?|2-c$pi1nzCr!H=>;t|0{BMb!bXnD-M{L_T={xWKYC@n?JX%btA)W#9fo zhM4CfDiaPJUMy3mbs0h-8Vd<@&m&HN|JI!cl`F9XWF2I4qrss;v^gF$6$jgpUU=iJ z_vFyQLqzyV1F9dIn!tpJEzh5$lvNizHirLso$ERl7~mz1E|v1|_H*f^_?XOMd=6L@ zGJ-iaYDWrlDVdj^3co9kdroW%=P601I8Qlm-F)?p?^`*xDr*vyDZm1Jf^o&e0;EJE z0Fz!p&5*F;-$GH&|8cPeYXvE6&q)@N0A+&C`WwPiK3(bfD^k^LZ=@TXSbZ z&JIh>T_y9w?-4*Oz(SlG*LXe%heUY*CO79lwn_WC(JKC#n)@GLEC6%i%uO?2zGv@& zFei@%jnuuV5<~(DJ)SyvN%WF=Nc7@q1e`s0K~ew<3JsOisF?=w#*o2~_48b8$t5sI z1K?6<0Lbt_=y0&sAB!iW1&xi306KPtP0Q0MV*oJkUrG5{Q^b$-VqBpCfal@zm8)i6(x_xP zYriu9FkdtFJL_PSIs<^ppEwVudr1A<39xM|%5R(TH_r8De2xY|Yg?;f%PeMyR8d5r z5DUOHej%QN?YJicIM1Z(5ir@(014&%kELz12=tp=u}T|OywX0C%)h2>I6jO_AVq)$ksccJvgu51=w%_a+JiE}L8L0oV$SM< zEU$kJCR20Pa|&WE|DHC^hl^_Xi0o2dU!&k*+)M%K5Ah7FxCp(y6pmL}|Hi}PPp89{ z&w&NE7RosontS&jlwG^`ApLYuPMkPx=%E?rr-Z~%qsEvsDFB-~G8HPR5If*UeF$cy z@fVU#Aq~Lv753|F*GJ9?Dt|VDTDg3w0pT(=MDS~|sDPn84PrKh5L62mpv|uJ$Fbdx zZQo?=5s;X~mU+x^{zJGjuD6{3%>{h{bd@X#F;J)wxu_8kw{*!OS@-x_*}P?&oH}zh z%yXKsWO`gu0=}14Ur*&}-l zj>L$le|m^i0uQ0I@yW;5AoM>oe>qes`FskMW`2K~D_6UXCx7|!RqS6yb;ubxaOjBa z1VI%I0OC(%n8Z*q)3Vtq_PF862rD*Rh~DR%aKAffjSfbeXdt+)IVKnr!m{_edX$9D zG8HgvJ$?A_F&G51OyvvHTkY+wCTdVbh+mQh;X&9yP zaK5Gx)iU!0e`6$z>9#ZIk+GrXU1P*f>FNSK_6nrqR+}oA!l(d`ll?m45mqP2#9#p$ z?OIO*;5CesPA5!6dvG-X#=rEB4?@O!kf!3^&n@UArMdrcpa=9*0ptaE_L(PT0Htzg z&YU-kQ$0eLY*=H$k>jXLi!H3qt1I+?3PFVvRbAmK3wRuDU|M`+^#k(!bL+tTx7d7+ zIb#}A^deQFFXsyZbi5b2W%UYj_m?ovVm|LbcvwDy@+bM!9+ZYAk;|7js!@=GcZmIl zh6X#oGI<13nJ~0uXJTf|b{TG}RbFQF>K&HUMpUbu1JEx3~4^so6O4SbY{sjLq-D6q*xI#9F z21&;~F6U_V=rs|Ef8RYTO?dRZ_di64@|elHGpAX~lDRvnAPCBcLd=2w*j14^&LrfI*iCwE0q8FV^nO(op(Tu4V zuSwJx&+X-xo|E-YJudA)dAneiqW()`3>u2*EWbrcx_f%$#K}`;?y~R0hg)|b&wdnD zB>;<}vW3mCm}d{WRAx-Ws4$gucafw_=EgI#5bF}+3aI#-EzTz795BqBpvsOTEpQy? z!6*TVtR4hlaNgWGro_-UbR15n0_Ijwp!1sS+J6n>m$)XECL1wOViKnQov=~R?|tiQ2EBX=ps1t!Ps?b03d#cOkRBMi&QqG)*DY*n z#A`wWY5u%9^7vzqm^|*>IkU3fT09=FWE6K<3IU#@$x&0T>f|&4tZX^`o7dkmd4AUL zGb%%c8AIxY)g@tNNb)*Qe>_u2xsH{YEGLEQnZfD-KwHi65~uLS+waM??YrdOdsZ03 zlFO`x>N$MmxNP6~iBXsv-gsB8T|6(bh9;9kXHCBI zmVG+va%wna6hb`gm*LVyJl?ZN3!FZ3L{1_ykTmmcNLD|1zbsxf-<+E>fGQ*s5ZLOk zq;>Yh>VS?hbc7lJQa!{!X^;4ah|g&@BH&LBT_q<_srjWxId(WFMZ)9dFshW^T3sN+R#X)xY8u| z0||Is`)GRkKmOUTOd8mr`nNB~qv&zSfXNBdifj+aq$Vq^@>aLQJaHn zifF(H4DqckjweG(Rz-(zlt}|*D*n3tjmv3giEa%T(l%M}9ZJO+m zF_4JqFcOf?YcV`oFcVZN2GtLPi9|~5sqp#FJ#UczPT{i!ajn<4KqVTqzaiVf;A?vQ zZ*SZrKY;{(w=yP%G z*a^c{$SC=<>(`lRfHR23lkh4cUI4NBU=^ON132eV_qJut%%}k%Rly0Jwl|wie;&7E zYQbkz$|4ewo0>!-yGX!)|J#3&v&WBs%d5-Kow5nf=s2(imQORK0Oy-BU@e|AJ%7?D z|Eq6()p-6*9^X5?PYwG=8}_H+<;vkc+q+->;=6wV#&N3!6) zijH_B!^T4?qYMLcj!Xq?0jI$Mls4`FiP4{Z=U*F)fLOi&2NEpf`RA|oQ>`vwi(uja z&U>FpX#}9ky8dBOv!3Msa&Z07agXL88 zJhBT-wpO}2J%7oBrDpAIZ79>40^*ic@;P87o_g}JZ0lk+BC4&a)q4L@p#AC9!w4X2 z-w%HLOVh^s?6Ct9G<3*J5a+m7)?t(;RTT!9Ned*?mL;=3q?yJ(pTDc;j>}KM+d&c` zUL(>9I|Io@=F0FI6t45s0c&ymr?Gv{832=^OJHJ>Dj1?asZpjW^Izpe0`9warTqIp z|BgYo|K(r*qZ~i52Q-1rrmTuTli*(@`x`P41g17eCKmZF4ZkgbBkO=SZ+_GBMR$ zga7__|BSu{=jA(p^8510!w*QV=R~4huSH>(K;pqx*~)%4c&c(qQJE?y}A^Z)p7aME@{AO9+t%n)rqK8L@4G>T~w7=&3qg8fHK{?s8_n>_Dzx}=Z-9LWcD82gjPGfwL znYuz%?Jcgj?>Sst{_d@8g{p4=S~Qp!?z*8-E$A{P;rJ9qrJ{Q5U<$qbNr z@p>?AM}yq87cMg+wXjWalhqBR1@6VTTJ6k$vB?DfgG|K@eirXNXOW*7H2^9{r~vD{ zh9t}{lJ553{hL3KKmT`s3<8|>=yl#Csevmpf_AuZ%e=0FVOF062$h_1cDdQyR#Mm8ny6jDe<{uzaFb zgVn%A}WdP-Vdk-9vC1@?XWbpz6 z$a0EI4S+lB>W4H_ZEa{E(s=y-4}T7pzgJE9X{@osM3hLq9Hx^dxAM>HLYN6bs=PMj zpGe_aku->aNnX=${q`#WOHK(505T)uI+oEK%@ip24(bYTUKo&gHG%;XRjw$q1^{ke zgGK%)wC!(oL;^z3BbD{qhy@^XAjkZ9v!xA@0U8GEXf}w50Kr<299$O`)Q1fbt*LiP)XFkxN#{=G`fq6F_vI7EAda27y<};$89Tz!i8i7 z>g1<^DCenlkIES|GuglYFd_mU$(wI)0!MB)P@yR6YH9(YNycf2~H+tAP;PoM*!GXThgAX*A|+t)RuqD~kE ze++#0TcNxNEbZ&-lUjtgQ(o78*btn?5?qu09yW6E%lvVOI63f z7BlRC^)hSb3^N{n2u&ZB9FjI5l^G`Iq8`?F$BdW4s(~JiYrXxFPUYC!XkY{m;AQ@I zA>W`7qaPXuuEQJtOol=#>ZnoA&Qs<04k40}8M#cHJnPE>D{I1jlw1vhd%-dk^P zZra{VMgLbBeQ=DU$bzzP;e5lJ|M;ViK-pizqSS3X=)*vGv&3uXuFv4YEv7PUwh*>qSH0HS2mdG5rC(}om@L@ciK zQLzBGqqK4!GgC$z-rOkfzz84@Kr>htQ#QOk9tzaWH@J1+C* z&N0k-Cr_P~e$?1IPs?@Pqvx08;%g!dnXLEaFT8=p6wvG8?=nB%*WYi@;{1*%>%>D3 z3s?Z;S~YD@xYwK$B*Wqy?(XR^`^E@mB+5jj z5?QQEHd<9;Oa;UJe13*-JiMm7wsU6BG?6*KX8`@R37)2bR}DZkEu+T8QpTbYfXX0F z^QnK^tP!B-|4CGzoJa&WRJ=20Ofw~fix(}B^-n)xlr>wP(xavVXDK0-IEi=qu}=UP zbDC~pHV+Ug3aI#NO$k(+v^F;b2|Z+L$4O2^Ml>SGc|4>>jUqSeq75~6K|g!;f+47( zEMK-HYXF#Z2n_(85;v@>aH0PT7ca?g(AkgPKO;m5+)FAj*UmQ>hX@u8HmA%@D`9qh z92&$YuuXKxEEuYb7R;Bft`0*0RbN+YTR)E=g4J(Q7Bp)2?>~fp*Nb>{LeljOl0if& zVGUHy2^xo`PHDKR3SPqut|hPOhPO7FYMd|p)(crsV3>|83piF`H9#|U#|{3j27r_) z_rL4ngx58;+f$&q|H)OF~i#MqQgOiccGBqy&sEeKf0aK2w;iRh-p8Z z%zA_Igf#%*&9FT&7v&_tPX`VjF^c8L(c@-eX85zAp)NAd%Mk;yCdZ zqaS@F4SDVQdb=Yp0UtT$%@N3mT}b zR3RRvtE*F1z`$F$a2~qvb((qWSoX|Zh0#jhf}=-I7^9Zw!&FfkQ4ZoCjOnO4G0Ar*61Fvi1 z>C@-r2qHJ9PMk zSyD$2nhKo8XWT|FI5$(KKg6hsWcBv;nrIe{&-3Rm%BOqw$!4U9KHR$9{Kg0(+f7I( zqrU28E({9>eHd4lGrjn!w zEhgL_nCn)`23P!DDrp*&j1El$KBcQ`87fv5nX=AD*Q}O%43l2#UHx8n&owg_yU;X? zjk69MI$}}(xpAG##j0f&vyRQa-N}e%*hMyAl83;jPZt=NOW$AYyoH}KS zX7&=enG8REsPP7^X5f*o|%DyPHjDcp4hjFkhU*0w^y)fwPdkS5*4%zQ0A@ z-L%EvHCRGQWmt+@`YffhH9W#z0%GtZYf!KH(hKY5p$G0YDJkAZl3U%a(15uAzEvhI z@Z2-&3{^HGQ(HgY0k8a=DQpYzB#A8eHELtgF*8!``THWpsDNCX-n3WR z#D(|a)enFh{i1C7a2r5omqVU;gm9xMDa$XniktFAQXtQoHC^y&dH}NF=?oO0#&Y3++9(>>)c^=C7r5B$u%#QhuiIU+x zbZM+GhjrZoma!8w?Yww;-LbGC77Ymw0$&%v^KJsSL2GNXDSth6`iu$96WbCRlZi+p z0a?&0uri$c1lDffu^Xw4Y4Z5vYdiyB!m$AUB~4kKVf2R|LAK?9RgnfrR3ZA`muB3o zx%NhifzoR2zsemDu{gzFj0h-a1Z488h|bP-6Ut?M{y~IYPhz1ZBAQAdD(}*TvS|Pe z6A9kiXE*_9Y^+=%ld?_it%F1*8v!^BuqLk??+>{YqE`OdvN-k<0AOzrz*2_eAAx~L zul&g;*2?mwi?8$eU7F%{!sBtzP%TEZLj&WtKKHyVLPX&G4?Z%4M=T?yXWjtAfK7Ww zBaQ_41XhN43uy_qv}R;XmM)%9YUMG?0h9Jp)>$3Wd=p)x20*zP0hn|&0GhP?PbJE# zf*Y+JF8oAqiXIQct}M+Xn_LYLa!;0 z<*@j6Hu&s5BY$Bzp2Dm-9Cj1yHO+^gn>G{EII< zBQL-BtZC-tgdjds5}1f*=gRF~`@$+1Mg_4qO5 ztNeo;6|$?G$kHTNs4?J2Oc;tkdhDbzmImOFOHEiKA2krp{2s15c_dgD&l>);NkZr0u$o*g}?o|%z7MOSh@1iwjBl_$j_0wHx*$d#9Y^O z*n5N5n%Dd&A}=hJT{wTPF;vT_M}U6-wbUilDR_Q{s;L|>QH|w)m_TuinOu7d&QZhv zD&rC@@ac$2%)@f7<0np;5;HcNaiFLuiGf&sz)ox|C;RB5?WO{O{n(uWU^wj~Mbh6r zXmY%jKMf^^(_|=`m?bMF~p=i3i9YbHtZ|*Ouk7Ol$C(@A_m9SU6`IECh{Y zvBoUZX%jf7f&wsL!s?Qlm_sM1W2&VbFi|bue~>?08Zj2(W-TsA0jTm^6Dnk2>9x^1U?v~>rXDRrB2$}smIo!9HAebecfDGg;x=8=aVG&z4?`BQ2v zkHmP)P1ljn36F)xL;@=|#$ulg#}5!yDj&H$jd#d0pzbd#iK z!<+BQ02aP@V-rfCn)4dEM3Q5L*T%dy$zIN!IcLC!^sopzyM%nJfeCmlfqpl+EC&L) z@u46{Z%jZKqYB(IG))gR7E6q|aHKuT+;TGpGMW5}4kG^942lLB-)6T+L;IVK?tU;W z(~E1*XalP<)`9_##^AE0i}E~w2M!%iVyv*|z-TrSz+>XE^#TX*2I!DEe?k_&W;3TE zF;p#sCFc==fFuRtJ?O)|7%RF|1_nT@)hCBB_BsQ=>YTJ_1fVR+{{n9UvF3j@-dZgo zdQ-dqsf@c3;JDDa;1PJm1}FpuV#$YL5RwB0C1`8Q-$>QfOE3D^Z{ECBj-y>L(+})% zPi2$NmWYMb4X&-SOGhv=(Fry>Vi#s3dN3QuJ$KGbQ=3j!Ix2)>681<)8)G75AuO*( zL_v`M@G}R^62Kv(OybCQhsTS?JDCU#K~`sgpea4rXQ~TcK>`|~nUL!g zb`8u$0-6w^;IT1<^f53brw$*I;h;s@2#ice8x@HeLlyI7V8}!q2txEVZy6|mzdIl& z{Az~(v3(umj8i(5UJjV3((ZpucFtoYz$^c&QRS~P-X5pFkac#n0d%#{gjvbjH|i8- zM2K_8z0OcJdjzm=!N)s3F@V71C~-RtYT`8N&&2^04P{0q_xNON04-4~Z75+RGaWm( zu~Q;z)o0F_3L{{;!89;>!N>*)Y}&wAPgJ?-Hdo}B%AtxNS4NlfvZ-{EhKR@3)Mugw zgN78+WMoVX`RGB)Zp_(Gfi7KuCZz!nSOl$d{%7lOcy|6BcPhNiM zITQ9L+WI28&t+`SfvD=vO_LzzYyqO`1rIz~2-#h30LsR&&ou93EdE zq6kz3^-v5vPKONVZ;-%c^+WfYa)sqeFpePA|5C4M7&Ivq`)4FWV>YYSZSc_*tHwismB~z{hugjmUiegq1r1`3s-*QiU+9OE24yTAet8yoAhNburoSGG|?whw(@-+%!XCT#~q zg1B6J+Pc;*Id|@YTmZJ=a>9V&f-(TCA{8LH`^019u`)-0?D&anb0ivn z(NwMEh=Bq_Kb$i6g7<`JGG{cGVL2e{%p1KqmG(=})c}|{#C68I8La>BEU5oiQJGN2 zxv(#K!Zc+eIS`w_NQzj1+Psp%Nb?Wb-_K-Q+TO~7>bFl0k^UifeaxccQy!BcYq+^^ zZ$a516&jU3OAkN)^7BTCG&D5iVFa9V98Y{epj0_9_ zUK93&U@KqN!1G?vFe)#6M39Zz5cJ7oB{+}A&T~lFvq&d;pz}4}e7ggaJ4wR1JWMZ~seN28pGo*qnbJu0++5%0z(yy#N);0=rDO ze0TelP@zUlJL`P=m=|_;O8r)U$067y5#*KlnFq-zOfd?MVeW1K3=MVczyRag25?BB zS2rJ>30Ve71ib4`C|>E}c$E(N=RGnyVkl@?B1sQD7SFZhjnr+-goJVoYqQiX7SC0N zlpjDc$YMxLnI4Xn$IN37bM~2t0jpu;fxp9ha0z9H7cXCy#Ut~x20)p#2lfwuI%NuV z4Ce0XYTyG%R9XE$-HkbzICCrzP*VUZW!GDP3h}YE4;$`vwq_+5Eo_!j6KMxJzwPbq z8W`r^x|OZq-@0>g-yi1H5z4jw<>^`f=JmJbz4t#v!f$S0*Wsm}Bu)1Xq?OTk2O6LPeR6+fJp#d=8jz%>ARHSUu zRPj7!KsE`Y0dN|%)@RP1HvlVoTczm7emCD&?BDwD(P&{Exhw7Bqs9gO7)lQZdwNV4 z!n5bjOAWmLYrsA%LjQN-1Xw}f>d(hB5)eiJMse-C&7ctykHxd8lDpRyfvuTO8tw!d zdn97=!`eZtJPDH=kAxE*Gmm}VoY}^x8@27M85zpl^|i``sV{sN?+4RJq#$NApv=&9 zg#>d>zZ`T1K!FabtaYi%%KvC)FU58#w#opgQbu6FAWMY8yz2e;u9SOLEjOiCGBjWm zQ7jfz9LK&Zbz^>?3c}GGQ;{SP5vT?I@TpU0!?QWhzW{x?P z8n+0*=t#c8F{pFb@$oKu7=P!yY-QMjjd?Yrc+!x;(BWtSCF0C(nBO_ycG(B0K1-R*W}>A z!=@cF>0e1ubQ#|FgZJMf3(y3{&-s?BLcqV8#48Zt&f}utl55X=_kL|y{~z8GQHP;&IYN&uu%K8ljwaysYf&YU@2?z?xD=}1T596cc}jI0)@ zBmc1ETAj!}cch(a{p zX8u*Bk{JTY%^-+p(gu-FuRVxRU1YMH_vBt+B)|tc24$-=sN8c`%1~J2*m(}zcNEf5 z9y-$irkVr{mAoIa1AO|W&rr1P09g;#5Zo* zEL%SO*woSwTBbfOw3Y1rkA40jPJr8b5FgdHb{K-0@BY<4$bb32|5CPnyekh^aJyxT zH(Mt1FaU-iY1rZwqSi1$9?=aODlx)P*yyJmg8+rcWFj{>cAi5%QBn|Hja?9S|0BMD z5ul7`0F?1l5VN|bs2Y&`E^tE1(uq6{wfph1sg?RL%1Hc8aLLk>m zA-LM$3-($A@7VPzS^`7)qfUO$zJrDXpxM;VhfPtW%9MYx zYeG*iY+3{*+}3H(-~aNxp8^SfRsQ%7f7dVwG8z&BB4f7&oXfEohU80ytcGG!60FhB z@)?vsRX9F?QQx!8qeU*iC_v%V%NVYPu(HF||3|l?23|ElNII2>$I2NX85A%Awo~Uo z;SW@DOW;JMGXUZSr0oJh<=FEoL4MT;`ju@%YJiK?Y))}z)P2|$oGEmGobT(4T;X0 zM;;2VpBF{Bk*CkcJ&}lCmv5)A8zL(Z_Uzqf7z&RbJpm6llTEus*OEEc1S**v4a*H} zGbO33D@T6&F^wu7bN2X?qvo8ld1TzQZ}{Jl+$O=%G(<^!SjC$T>$rm${(J8BfxN@qob3? z^DkB|n8x`#&H$3ZTsV2sq)29O;G6RizODK5y8^3tKB_j zOPX0KeHveT_a8L)1Ac$l10XsKm-A@3unvK8_}uvmW)O5p0;uxgx4C&5fXD2PpXU;G zFN}&ZIk5D?8I$g50S1IPkVHIDu0YAL^vw}oDvu99BLL$~0Zc?Du05NP&+@?;Dk>A< zLFHQRX5|;Wp!mHa3K9d2_kd$!VwBE?%~&(2y|%f{sqp8SZbv7Z=Q;3P7^#Wk_}zRD`65y#)zj6{ zA)TEa*;3Fl_usPm##RILRw^ZMqDlyW>aFrGGypULpfc{DzZz4{7c*otn-FgP<*zo# z&Yhncrn4q^{f(%tcJ#NCRrwbZ=cM7?vr?k1wN=htxFF|Ggm{VjR;@6&fEhEUWvc^n zp8kj-^T~SpxZX4#j-w9&4S=1Wd?tJM9gt){^Zf~_fd@_iAn%XE6hx_v-FLYzjKp{_ z>TnE7bZ~Ik`y5ghh+32*0>JND$z$fR^Bj0Cyg#n3bQDGaQ!u>uHBkCg{#~8zhH$6? zA^=%^V|7o(>YJ)a0bnwkCO~*=zHJDkZGcrKCihPye7=*0^^AjbY{9DHg%Yk!1sRia~Vw~<=eme4bwy@=ba9MLGgrqdYz{~VlWjh zcg^b^YY4AlV)PBJL!Q2|DF8J9#`}g?D{Lml49QUdCsg^X zj34MP4E++x%r)T9;bZdphK=&U=52BX7=XIgHk0QZ9v(4-IODyzjrDuM4e;;dxtJM+ zkPdqxurnef0rc#*@Aw4!Epq=ot7PrkhmeoHXcYg|tKA0FcfF?%4uM4T(C5+G4U6u?ux*D0?Fkcvvlfm2+_XaG=7f$H|3zxQML z>Cb;{z-D!AZ6>tIu)k~eGydHOV|guigX?OnGI_n~GvlBrZ^oRM!eCDSk+NI00gP@Ip5OKsg9yL==DD<+J8-4|DoA z_w>g*Eb9zH0@@NdK6V{^7qriD`EmP+!N zdF%~1?wBpxjOtQ4q9mpOOw%FTG$~=Ju**#Ok42VHcU=jJ8UPc(SfSy56`8nFHZG2i zx$fM#3$ka=epAy9jcdC9CGEl)jH9<74%c=j8G6F0kdNOqb)jePcgeBD>_I$kA{jM_ zz$mYT1vD~T%kvm7g__Hy2L?RJ(mm5p0M|6AZ#9Z@;ywKv^FUCs@i@<(J#V5xEMr{0 zbg{H7ZO-yT6eBpAs9p^Ti(TRI{^V!BmS6pPgF*ijxSA@#y>}4>(qL2hGdkkPni7e4 zrKF-0H_=UM0F>GYz(h*mA<`ZIDiZ?wi(%p9B7Wt{Re9^3P4f1;?*l;9E6sL6b3{Mm z;q`NoCOV$c217m*h#}SC;@Z;EWH1WG)1&8^RX&+8A8V;!`R@n7RX`9J!|RM5(7Ud~ zLT!{9J?)6g=ZZ;)rVq?<@mP4g^qxE5?K|&rB0c?EF<=;>xO92IL@gM-V038N(#4jN zIFUz0G||WDN3eLT7fzzYy*|XQM41k97zsmtJu(ENE}hpUGrX5&OA8$2U%pJDk|WQF zN@W2oAY7>UP>f2Y(%R;x;i)rc<<-~Tl;42opKWhT$qe>N65u+9 zwD~&*rO(Jlvb07(CI{CK*In!ByWgB>Pybfu2NCq6u?AC0N6_NJ1@p{sOp)|>;}gJx zABO%zZ=1)$xk`W~kCDd;P=z^me~yld{y~vq@ zx=b|yR3-p4m0P4voIEZ2_8*e&^QW+Owi!=#WF%ZVXS4l_J{-8w5Qu2S8I# znbHZi!FoeFG9N@bwgvlF-Ew^l7cv4Qx?9|Sil{D zYSX7rl|>8Z%VUo|f^xeaQx0|pdEt%kZI&PY_!kIgpTX}n8DgIByFxFvR22@sVNq#y z)+E+3o$hd*Xh`uI@Emlge13yo6+A{pglMQx`TOKR zcaKjAPzpP0zL)HO58S^>o_OpLGf(~0PZ>M^{*lFTP7TySIOf4h>MzKaOeF+?X4QBSpxjPnNF^DqO_@C7R;L?^XJcz zl`EFXS~Ssldfj8PVE)`2f)~X@^*huxJ#8-h9F+O^UJAirEl3*-4GqbToil*U?>B}I zaSy~e5FZex2GS*q1XOB?r~zc!^h`hQ*AB2k{_59nnEUqd>IY08gks7$YD2&et;fCA z#MgjV`51K7JSHAnHzGWBFcxDLP#WbN4vDbB7Q^#wWNHmMKa%Y+yTHd{l~jG^^v|H= zLuxezFb-(|%+Y`yHkyhU02&2QDc!>EEsCU{Wu5vv8#kNWIK%74b1H%IH*+70H$C}& zc;=(Ms4br}Pd@+h3-Y-apOM9j78peJ6w5n5=JQinyLc0!K|rHo$>N2ko$j{nJLUZk zK9Uc&Za2n&DaRw%!RY9O5FJ$lMKl4JG?LDP%;@_+{JE(Vfr^#&Pd}aq;v|4G5syb& zBIru}a#Yp1l*;z)yXCL{_FoKu^~#lQ174+~PNt#DI-R`)M&R@1%o_l7Uyz-^bnXtc)=4ReLTT^Yl(0q=7vAjf67hnt+ zcMdV2wi)v0V$Ct<|7Y(#xZ^mobitctUv=o7 zZ|9sf=j{6#d-j|?Z+G9knK#ojJ=4C`l3HG(L`ot>N%W%Vy*IEVJWy4+_q!316_o@i znyf-qAwnlr00>k?W=6(!-~GPnQ@Ny2Er#M-1_}Zf0HGbEx+^LYTBg_0apblICM8pX z^>TqR7N2a|LX%Ku=YLze^bR4yiEG23b3eixnm4jct{;;0^MejgPxtvJ0!ITRRr~iJ zrnleyNalh$RdK1Pp`k7>jFrmg^aNKxX_Yks0g#FQjdM~m55BTFHLyY28OW5HEO4U4 zWq{HCSIak`j$d5~z>R{|{xOh(KhMB8M82#NDw}uIhEG?BCGC&MAI5*+YAel{w}}4s zXFs8rUwV$_&6%BJ{7Kr(l4>rt!SJrHv7W69d(uH*8xVUeU9ylPxN%Ycuinrcb$rcxP~2?#~LFs#Uwcx-4SN|woM^x@=kj(|okTyteWEl>@rGVaga4ei= zr9B7+;TocoW5x0%QX8KzfK>n1mFf0g89!p z>7AWdX&Zy5w&@5enMg_m7QMc_Li;1zfSNzj{)qf-<8LDVd`7U!@recCM+_K^`x(VO z#6UoF5J)Qd!bLH16}3t})-iJrp9#QqtmLu^z?9p09UVD(oF-0erZuaUb4K7vy6@gK z!qxCypuQX7%^zYNif!9>3A^8p-Ft=W0St@yY|H_3$yel$?N~4c!bX|lw!dlg#N!Xq z6OTP4=W7E5ORDy#Y>wEU5FwCr0P<+`!)+LL4aN;O@(ljV8Z-38@8{MjwEsbu-n8UJ z$(f(sIMWhQ1Mr8MKhplj`1f(^A&h^%dF73f$%u+uDrhI1I%Tq?uhH9$)N)5h2kFW~ zNgL{XN(SSe;+|D=>m}|jFcc?QJ{o10^DM#xJAXE>uT zOpYvxj4TX}4(4^}?dg>s|F6E@NpEqzfB%8QLIqvR&Kp`k4VOf|z?;T-9OSvBcrM^X zShIQs-OU{kcxF<1Z5IPAGTnR^K#jIR@eyp4a{%)AkctP!uPy`NM!{t$Ku-%v+HxryL~0lq1GM@9?GL~oYW{ZQ5AmKtZJziTgMlzL+X>uxux8Z?+Q0S?bwfv< zuL+Ei5xglSK%h469>2}Cd8FrYZznQP4(akGix$#sE)OW) z{Q97)z~BW?A_5Szt&KscoaTpq0w{f3xJw29-op+7-Wz{o){5=0=5zhvdIH3n;?Kt1 z0V1u?*$vQX`=w+2!x*$WZG&6}V6;$}G<+MN8*bz+GQ{ZNXhwA2dE?DU^i2`!rRS!M z@u}$ZN2{Mf`=jPxh{%6r-2qe6RvS5L*N;82UTWGc?|jI{<_rtHR7z=}5PAr=dp|u5 zkW5R8en?zB6Pe~K6JQK4u2YS0FL-iak8w7jpN%;qJE)?*i8Cwb=){Rra*ZdjavqTS zl~o}JfNT6$u5?jrd%M*6_a8hgw7!E^+XbLn#fBH{c1?`iFe;IvoR}wOMU$Zz)XoX! zd&k0g68WS1Ap~Ba5>=rc4uj@000q1Owfb-%KsO3N{j2;;q*>cJ)s2mpbo3pijR+`H zHI*zF{y_4BK7WJu|E`w4!ej#CIa~qj*RAC?w|4rR>13(DhXw^Rfa{YfItL)=IoHvr zA?y($B{(cVrN{h&flKA{bc>$QA9LzIlc^NsKiW(Iw1fdi7l9EH0_ap>Qw8_~`ymW|v zhO?i>{x}feC~V5Y#N?=e-6)g;Akw(}H9%BIwIT|RRc71&qS#Q4F+wDW_eZ-F7}LP; zhxBi;Eb}ZTKq(MH8vw>>F{i4bcn5l9q?nB$Qq129TF0M{u2I48i32vfC` z(@q7=bnxRN(u(P}z$jAyLYb;1eIe|Fv_F{mY=%E8|jN-)xbA^7t?S8`d9&v2Bb$F%8AHm96ffD&N9D&!95gW?utYy zouhhabOJDkm^SF7`~B5s=gE$z&NUX4EtJFHUDGvCsBQ-wkfNS4WfHAkwT$Lk^wfUo z($HVG1fY2UoC83GQDdssN6aF=a>I?hf&Wm?yui`eg^QP%m9Jmw#Ga7m_>qFpjHE9j zYY6)oZhuqzFP65cumezAQ$uU-y-QNoXU<*_>e~dTtgHDvm;`pzBmE%_yhGg`A~dM0 zqc7jE7cN<}fbPBLE^29Qqr=>q*VWp>b@+N};>^I1Ze`1N)g^4Kg=)DX8Dw3C70gpU zLGZUhX`z6|NEj>GK>?#50Ey5Qm@#Xn2>v|w@H)w6l|kx15abNmAD00b1ukYI#*o8& z=?s<|`4Xe)V?i9C+|P2$TYukxq->+b0QlL}&YL78eQmY0V$@5wjV&T)I4sAHpJKyW zBT=YC-N=%pM=#PhT=<#ZZUSjrta=LFxpa|~Ru(Rt%bAvDabCd22SfGYhEG}j@NNFx zUPh|dQUzx)01#B%242CXg>UwLk4_Y-G8MR1$gEJRiy0$pB=!H;qYnxeLCR(^uN2$< za=5Yd*CUD1m4qNrNp}Hwo*-Z<^+Aqt#En8Y04M>RJ9m-Jalba40h1bzBE@QIz8wEc z#9Y(dOfxw`g`{teu`RNVGiYmL<529Ec=ex%>P z4oRKJ+rs$Fm_Cgjy#F3iqg%lG2H@hi$CAYhY25?>5_|vjMHrmydg0$=R25rtqM~ zVoxgzMK%~KIYR}OOzg%}XD^~hAAW%DVsaxJKwD8n8LZn(30CrXSB5eGt_y$)*9A~z zoriM(+{nw5p6dYgv+=*cPQV3m27t^?N1&V*fd90j9%`p6SKcWAz($)gRXE{`6f0r1 zl+|!Ftu}cB0?ZRQQ?nDAbawze95&LQC((UlV}nrAq9qPsJ4F8BfsTv+>2Lpv+3F_H zzyAAg=?s?xK=)kF>3E$?#t79!EE#e$;K-4o0(%jw`~l2E$V$?OK`0IsgDj6V7x_p974x zlp|3@)HzbV#Ty0&UqUmWkt6R!ZGJQo3|JD^3MhG$Vt{&Y;Celbx&qF{g>>e6k-izG zOh6il2gh#p%4PCCr&+M)U1nSa{c|VZcNjb1Ffy6Um{&bC7!aWP;T#Kam9s04KK!7x z{jFNDj0|=r$t;G6c)ToP0_*#v!vKt4IiO+~ugtmSZsY|3h=H1NFC)fN4A6_Skw87u zi>L{}$fd#GlL$)X1*)ZwEe?{)WpWgmJR1a&oQvm0e z5AmFmjQqLg4@&AD))$z<4%ds{dxl

3O+!6m+e`sSGZYH;e;NQK~kl95Mij5qD}g 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ΞPeQgEz{vu;F?_v0t)d~lfg$4jYTGXQ*MIvhee}s^vH>IV zN7}TnufJeYw{~P4aP@mCJuHfR>2$!&U#i9*&J2u7KRY*rJv|~ReAlYwvZDft-_136 zBR4FpimBA-sS1ibvQOtPUgC_&?QWgB!U;VkX3X;%54+T!!aH@hC(?y`cqPUOt zfYkEN-Fs;pcOUHeW*@ci+QWO|7I*XY>F!ns<`IX}VNm?|Otam2l>IuePJUqf@qa&X z2-M%AwH2Rt?D$D3f5_YtNp=F7Y31^zboZK-w3_eB%o)>i<_rD=vrg49=Cc8rP`8;^ zY8n9LMg>e3IHN1%sk!>UQ}fG-YTDW0?D=Ltz46w2^v?Sq)8#W~Xk23xH8(d}k-wLB zu%ASSZ0HdIBQi#WIfcPzm@|0nJ2*P5V#A33blb=jyEC8+gck_z;M~LkpVp3L-01n;a**IOcom%vn0g#u8crb+xth z+%u2Uyz8h6njjqK4`}WwmW&!K&(xuCE;?x;&0MLH8XYTgtrOIL@_s%mEpR z3a;TJB9G-{4(JAwo1V44aw(vZ+V8w_MH~*CU#gcg*nWzVbq#cu6)rEew20#aM+bTd z%}tGxB?vn(V^BrW3uTtON}U3@oE!j~5C}Dy6TC22@oT+4KhFUH$1)L2I*{_k0OI?z z&$rTV|L~?59z;b|4dYdW%Emt@N)dx>WKeo|@PT_J4Gx*%?zW5E zZ!}(vkLhR6_^J)DD3$q0sSJWI0k48k=XoOPr%VquPM=RNz3?o(^78YvbnzXM7GJVx zff$)z|LzTHIeU`dp;6lKMgx3_k4f38f|YFO*%vv%3XuQxAOBU#ML+)YS7`kMYXc!6 z{GL)3RK`A~OA5AeJbdK12uN+;xtq?Pza-H;QvU$@AtM#5t__@2<2<3(51l#BOJA$l zbR*{9%XJ1!c8SF4)L9EIdt^iiDMn|`o|m!&6fPmHj4p^Z%!}~MQ;+88CYTI}X_FiS zPC%Ff_$pt^6a3m+4yEQq>F@D5l)$(tNBKCy$DMo_S3Tph0b?`rH^v_k5Tcx~Iqm;8 zr*>grTQ6Lu8m@hU4<1s!g^2u3gb0Iy$RCJr7`q8vpMBuIyXl!HAC-+2#$fZ7aS}Zv zEspv$21slWk!4fQexQa|XrI7r{&wdAt|1H?j5mzrL{2X^aU=;I1*Di?dFeTNXx)8c zBrzsWo+$I~;d=X~Pq)zJmNqs-U2Fi;lAeaK!u3*kH@{#V0=B9kBIXkZ56Hb3C#r%& zv}`F<1t-eC8)twlWIqgi3uke_D}hx1hZ{E1?mgd9M_UKpTpT}$Nx|8XE(kJhftIl* z)wXyu_QlWn!oQ{*Q^o5?Mepi+lo3=3WQeJ*&(kq>cG!!dI({a=mWb>HunDtf%+U98 zd=6t`nPUTdZ9C#t-Tw%m>rvYQ&^E|-4#4PV0J{0+9N|~oq&b!Y;J|=1mel-wp?$5g zHY(ITq2%}0yC2d{CinsHs=A?0qSXP8{L?x*@(LhZ3nPI|w70jP`Z(%po;I1Dd-id9 z<@?VG;0%Ug(H-+?)yieGk>?CTA^^suUz`5}U{1;UdKPP)pRM~+VQTv(4QwJxWG2Lz zFouGL`YBd5krs1=zmBsSt5z(dg$w4=teGN)@rv5g&;O`Enzrsg50 zOn!>>6Y8~g0e)adOD68! zlSg?w%jhC)@fmQ@DnN~B;n@n-zNha2gOt=k0Ujeh8QZv8@ZZ8qV2Z(s2^Hp+x# z8@jrBIM7Pbbgs9qU$>TCe(^c3jo(L2jpOBaSF`a)4YrQKT8O+rQq$JaK^^U#3>51X zd;p9h`oGb#m=Hr552#*m+=$$Gbu2g1PidpfG|K!nN+)nkCQfYTl>S6(+- zJ^Jv20x~mWNcsTP$D&Mt2pWz8YTmdX0H~cheO8<_7+nFYp|4$&GWiAU5-_TQ4fXYO zwX2KH@Vq*iDjIEj=-9Z*S%IaC7YceFDv>8oon_&uU1I#d`g#W)IeJ3+_pAAO)T0NV z&&|*HS)gEn+dW5eGK4FDudy=bD&x}4nVkIx4l!NwQ98xA1*|!&y?fY6ojqp;^9j_9 z1#EzQb9ycN(pSn9z}F&1N>}>RkT8&L3!KhhzQo7>LP{8jG+krTl>lx;IK&_3pb)Ug zyLR=MG;~h-QL~E0G>5rHF%QRMP7!kY+aRs2Uu_p^-;JDJK6vP;)HxLakV>T&Y6He+ z;Pa|E0)!Y%Z$~>#nmLPp{G*rYr5B&270Z`!DPsIJe+Tdwejh#J2e}6Q1*7J7?D~d@ zi%wDoATGQoZ^US!Q>}xEpoEdVch{B)hC63-Z#~=D3tlNu$U{^NVj380kkJfwb;%=E z(=VF>-yC6av$F*p%pbcq;wL+Y)X`KSd5D_g5!;Ja! z#c;gx()0A-{r3cHcqVIs0fT|mIF6-D?x6Ye=7=#xgbfiJ^w6Va5r!BwXMoH42d+wl zIUKcXt|LaF&H@Tb2mw_sUNc5u#t*=Ua5RuTLDvK#W4s%(5HR>K3dm$E=hS%2V!Fe% zMwvwz>q6i${2MX~aNKaNPe1vH7<_aHfVJ)*N1$*F`Z;p~DQ090O!QoERY9I3oB-U% zj*cr#)^kDbE3y@rS!VdX4?dw&hxhQ!Yotkx5rDxo>r=IJmPQbOff5>?7pxhqg$vxl zaDllpfQi68z&c&AY>7B|C~2W&qa5)}PA001WPtHtw(|99h4D8w$CUs^KMQ~y4BXB% z->zCFYU=tgLxN2**lQ9mQg*D$gb;-OW&?HMsDioGnBHB7hg`6ePYYJT_{>Hc@w z_;25_hrxdrWYdQ6P*kr&1y%FIbwxWLqQQZyZ8UYxeEQi>eM$1(z%WSo~x`bf#|do zifn)ZupKr=UlR}jWn3E|6&E9*?gq#>M!=EMt)A4QVf4cTn-(IiK4C%Y6E?I#!#nP0 zq}H3ZYnQ$zD1l*#E5)}*AB1X)2)`G?K{BuvxpS}7bE$8UTMErKN zZqor9fFGiFq*s9lz_F0#%I};xdro@O5rr+7e@tj>krhbkI=f&Tkh+F=4BuPS-*GR2 zvKQc>P60_fnsx%EJR&8Mgic*2#d$K^oID#OI8tiZW_w=ZiIg7DA()V7MPjJQLuD2!MKR20-0y zv z;6R)9)YNE!7KU+{#|Hpmpdy_me@CFX!KV!yJj_R7&ur!GAK#dfR0yc9P!UV;?Gb3O&SNVNyJWum{ z-)FLw*Is{{L2`R#bAmC$MumDLqS3UbW-Uaj80dJ^hT#CfP)=n5mKUCTl77gM|LoZ_ z%CRXU zYLuf|MUSRVkAFjY7ijU-T<=9adBME7Id3hSsWt?0-Qs2x(Na29jB)(**v%?qD24*#TvO$y^>l)Cn$fM9#MHG3O^nN=JJ~S=kI~ey_dp4xK)7 zkonZd3$<(^sbo!t1x7WM8W2N^=w})e{480tkiN%}KLCFwN{3TrblPv)TOTGXU@`+@ z(Dn6?X<1ktm7*t5aoYqk0pXBGIpEExd%x}T3{uajv^Z1q=FOJtit_>2z}{~U3KAb# zDmblh0xEQSXQ5OD$AZxxZaoMVg71SNH3HbSuYaoq9TWLE1_2b-xFO^!F`fYXre4+d zWUtOD2R5hF?|oS~02&AwES+UkTTQryPYCYr?xi>shd^=nBBi(%m*Nsi(W1p2ic_q3 zacP0#R@~j)EjQo2cdh)+%4E*WnR(y6pUvzbG%C19TDxok!n74o606#650!+v_dIPjT>l6Y&7(aYF`X2MF)upErAI*>Q%M4X{y z!Bb&wP>?;kD^aZ*(*S__zGv4*Oa){#I1yxE4zNo=F`kZ@(B4!=+X}A(J=d`G0*lwI$v=5MxV}~-Q&_%>da!O=Vd}}^WpnYBPT_!6r9=~rd-aYs@D8VoB2f0qC z|GSZD?o8TI915DrJnuhmWFcZ&`C~s5d=DbS!EfFW-RmLt@FWm48d8 zKKp?WsgLu+I1SR+8hQ=43;@WqY|3Qg7J`Ga@D(gI8*I7&`n2442De+?0Z$iGzs3&w zGsDrpFrK>8Jne9G6kBA`_usmpnihUrh#>Du!mTe1FNuwz|NL&hXGz&PYW&1YJd7c8e)p{tCjRM^qfh%;$^Y=g|?HSkdgAteRWglL(!u_UK|Gr`1 zNVDtPE{iOb>6pvP)5Y(uq(D1WFyUomX>Aak5)dLrOP z&>mj5+vFCa4bw=qyv`f=37Cma6gVmx1n&+_fKXS{oQ%_K?skHEDr$`uKrzU_MydlN z1>00}1L!ny_1C);kmbY*+N_(0EtnrT@z1_>ld$r1sGRZ9;cpCp+z|&3Z>!sx!e~|* z4p74AG_|?X!K5%1kq@|=&fZA4BMb$Ti_6L}4o{~hLs}i`;><>O5pr<-4>z$p`*gwv z!rQFNuDB(yG4h=>$w42q=L|j&Yk(OC=!<}1`Dcp}G)XcQJogxa@aN5#%xz&j+wUQBG^q+V@^wF=mNT*{;wRF?(lNJ09f@G|CSpHl~l4>NjVo(rc z*hFeEgnmda&m-|LAv6r-iaLr(ee673`EiQavyU&KZHPAaC zL-WEzACU>EwDT&-La;o$=5XG>=q-}2S^-g=o>PDO;p_%Jp9IzQYL@q=UEdhW)ol28 z13sl(Qkzo9^tE=*v#bdhW+paZv0?Rxl@t0Ufjyh@D=eT_oSU70KwZd*DG?^Q+BwPV zm_NToJ-sFy?iWK-Dd!6gj*NJaIs*p=a#JmUZ@XkO?wvbz?tt1kypr)C)5=gH72W7v zFckuv$dMH2p)ucsMmlY-5voJfrYp}PKfO=>c>1CB_R)LpP&IT8ZX@UH9q&b}FcKzhi^8zoL=i6f0k|bAbWEKsBe88iF6VLM3 z&Sh>NDA|rZB|f6#>;5Oad-Ys#WkypI-*qy@#}JWJ_Lb0eM5mBic#&2-=yVROI3)vv}RRk zR}q;zBKp>>u~z&A_fc}Q9j1h$nj@_I+aF#YBQqS;EOvYX*SCCq!f+A_cW;5qGm3Df zrRw+EaUgi%zp@wA2vnO(lIc@)6m+{!M{NPq42`RQWgfJ~p%EGJ*Ge|SU>9*5ggjKm zuTb9J2+=5ui1=!#Wh_sl>C8F~b^M^>X78_;QGHK8>zm(5ozu-!8hmDvmw`6JLW}&v#pllT{ z0+NYxEuJz*qWwT~Bj3(gNeOk(m;d}+ba&_G#@H0Ocsyje2=lTN6#aLnN<74$bUhQ()| z75iO(6vQ%eEGnxb86OLL*nNB0n$~S?JUletB|i4yksb}9N|Wfz$?r|L6aTG>d%Eaa zZs5!77MBBVEZrEAaazn1+av@Py~zA2qTNum0)5Xob6oPX3tvjmIM0-zLm88$vT%)2H5>BB~k)D6HFFD=j zFACxyiKnyNCGs<%vsctygEyT{j-oxV6G|w)Hp-WC$e^G*Stq?oCi{-3B*n2a5JZx` zel*eq$rF6JlAxMzN(~NNYs)S5SZO$ErkI#X_7n0&a6y~(oatfudPEY=ZG8eIiDLcyw!Hs$jNr8sJ zJm-x&@^aAX^7k*zE-yywvovbHKFy$b3yf6sIBq;rk2h;Tup>bm5i1#jH2fhE0t1aZ zKVH-0{xvVaN&hHBM~j94`3q+l)%Wf!3z+GCIS$%EFj>E+#6pU-YWmAO*u?a&Y5D%0 zv3~A_Jp;|*^^cf4c23grYK{7T3mnK1$Rc||{+Cp0AB+nvm%aB-QObWUdGvop`h>** z+RQc2{v7IX2D@>7`DXtzHVLq zGbCEooPW9zL|f@SOQz!q#r+~b()rcsH^XOE`>nnrIW)6G70bk=*xopORz^6*cyLHU z)D#DtrqGU(67d2oZKwtos8kLl^@_;v+pV&v%8|Q_C zwfUm3p_MNveq9f{SHcd+V`UALi?F`8$g~7(Acsv05t`Rwu0Ry}J@z_5K~YR&$N#pr zAr3m5eg71;oSS#yIu!l1VGM7S_+rG>C!LjuiJjYCnl^twep)ApsmD|`L98zqUqO#^qziNLp zdN&|1nV&|XD3jshc<%3bZ%$!jc_7P=Y7j@?g)oSldOyfHt zebv219#O@Z(B08^ivc#vwRsekqR0%G9r1PWM7PR%X-4(-C+GWPGGY0 zWzpFV))Auf#~{qmDP3lV2T}xQ>_-fHYf)Clr-k=rmToXM59Wsdlg$Gx_>F!ka*TbyB!@mCJ}b z?xPSv7p&{m`4`?C!xI6Tg9Mkg;PG(K7w_rIF>$(B)CD+NK1vv&GY=PbE}6`umoV`- z>hN9bUMC&gi#GoI5f$5o5voX-h03CqUCcQuXBs%kkq|2Y{(d94Z3_h=RZW*<(FF;YW_``8Y@guL;Rlpi-tMtF%IqZq(jc~)j9iK&c^%)kBDTXq z0XJDm3#>Uc2e1hV3va=__l$`|=1&t}JI(L$A2MNQ7QQFx{dR2N>mEZCROGgJ!pq8W zrjY)(|7I?ujY*1X`)vKJRMV`g!i%<`@+!Oov8 zVgzimE?I*?y`Fi}i|Fng7Gf;LY!SckCDA+)=9U4y`)%4)jXeaFpDiFcA`H`{`)Jqw zt(*_2k3cE7zGCAgjtGh7zG30qY|tpmNJ|K^zm_%~f|Y_`w>lr)VeEJM(K_BQD`%@d zh&R24KB!3eRPGf;(B_?wo3mL24vny!Uz!|+7>=vwnM{g^d9fJJUWC67)w(ZJI` z=WrvO1mQJ;_20t9Mmtr|&B=G~%kD{scZmkjxc!2(7NfpC-LJtqeY5d{-2tV6&8GH7 zVq+bOdz#Bu1bTc36tt|NvBan%HYjYde}u2iYYKsd310>%>NJ^ zBG35zA%DNP;P-5rL{Q zBIy(C5cc9H(gLFxoaX24))zm81Qv>ccJ6z3L$YjdSDa;A%?@-@JaY66Y}uhS2Z*i1 zmya@w5h-{M5$-^#M*|4D_Y~!P#vd49O3F1(fi(@G4pWNh5+CiOaWKG7?jo>Z6zNjh zo0l!jaIT)4T5peuLg7}x9cbYcVyF2<%=UMiHZELfK)%Up!lA0P`|UVN*2`X;af>CM z_{*N}f7kQJgEcub5s+Am#b} zirhEqIt;l!7kFkYzxS-8jCx1)Sidu3l6net0WdSpv?+LtJjw#`;E_{7>3K~ZG5WYFI5IKH8E zo&*Q%sX!7`WYg6niI*b+{|{>64q>Of08t|<(PO8wx>ch6@ zLN_H0z!+9u^#0X+xJ*I54YmHzSSfR+Pey@*Oe>>5K6t}&VY|4; z{zK_ElpF)Qq0L|U(&oE4?H)&SivKcc&Sz|ST{j%AKYe9@`Xz)6T%set)X`iBI8qFY ziO0qEzzwQeE6} z!Trn!*zW>r-B9nv!r{e;VfhZIln91Fw>?`4dgd1)B-W2WW;E&O_*#4xI6Bb&y=eA^ z`}|1RKPu7N_|?@vrK^nhocFOJ@8)BO-`#!`Y<*3 zOI3g8(>QGXK}5kjiGZ9STvV&qnerg%8;g^JLmr%CxGVF*)~bWa&lspyjN$&uc33o22{&~*eROn8ji6Ku zB4lcVo)h{RSrrMwb|Hubndv}|x@Y~V8V{?7sGkIh`2e!RzQ)UumGT0ZYzkPT z1`~*G<(7Z)iKDFWm)Ak6cy_MKkP>rF^T*n0`u#r>jj~0EkrMm6rviUe^7d?!0 zkNZXO0Fc`vG`kRyWGb8iZAo!_8cb4D5pPL)e?Xn6fRR*S>-cm`^CGkCxNFO;%Mxu< zd;X2?_20OD=dZoM7@(x3QLxarx&}7dGI=2b#PkNV8tNM&HxH4_`87qda!lbg4f?N* zFWrxObP`?1G{~Ep39PIc!NTTMA3lgrCV0eVdos~P+pvm=g^8s;{c1+(5^K14PSbqD zfx@4)iwTXCfZm0R&!I1Bj81o2OoD34g74peOi?=Nm+KhoyC!`#z3;uG>RMI({95zO z;|cvjfTv4)?>OurFN_9~Rp>q}G4k=O+YYov-K~tc)EChT;9w zsvsi5`flaK>XDC#yu;&tFY2k9l0-xA=0dIDTw^^qq+S9=Vl$6t7z6^I=Q9{;2?i9J z2Ri*@AfZVYxc?f@f>}TC9Lwr`u|ubBTdMj`T*nZjc7j4+UoH$C72_w8y<1YL+>*UG%= zlGFQzofgr5`3((Mc(QPix8AMgJW(|hDddbEqW~z;K>TRwL^uGF{2ggqr)vWg7BKC| z4tu8(e}U)<3_ZNPXK$j)C!I}8wQ%9F8E&u5H^^BpH2~?xm>h}oEZS45fCHRQd&I%xB#XFnRiVn&=V!*L6Kx2bfMDRC}_teC> zm7l10^R-lw1skhXc<=P7K#K^AfAMMb3zOQyb<88%uXTTNf=&`W$6cT_q=vWqCt^d16+Qi zwJZ)*R+{@G&>lESc$N1Qb`^7{08=CB;@Z3pSi|Kua>JKVZZSUjyM*q2OJg z9W=4}mkaQBSOcr?ejEZR7>I!IM1lEFNH;$}47WAd-!_5dg$~v=LtrX>`Qb!J+D0KL zlQhv@8ZF>gk&$Y?_p9BZ*e<4=*Qb?-HJP0xN#eEWMizwJbQBh6K_MQ9YB;>W;x8*) zQc!5lO@X86Qp?&uXRn|F|y##kQhys6?0_Ee161s@8S%4_9&bex- z4Jl!3X~AkY$0y*%I;hfnNabdUdsoy$l(yEIgsh2xAQ}gSpeaAawl;5|rE6VMuC}f& zQKZTUZ}(N4d1c4SBGw&FdpJb{mTi;o3*WZ!Bh8Q)&>`@GHge&S(g`6pN4~=Ni5HB1 ziTX8R4J2(DWo%ZV4#D9qM2nBPDpw$FIHh@>|9aKquD)H!+>LIn_2RQHGH!q*S}xzz z<=cmRb&aKj{K z{)a!rVX{ulYW*4aa+N2i*P!>QU;bl-42(Ym-Mb(iiJM)0M*6EjJYT3Oay?DXYn8jH zd7tF@8eb-ncW&9nThmQ@m*Kc*9bxG6b?V}BaTP(dz#;%G^()xYV%YstfjB4wr9UYDPu4cix^z?#9~_M5m*xv}~t zy>&BWymq#qDyx5Boj;M(xu<7S>_q?Cq;%ozBfF)y+1skhS~31B8I_pODa1bf*vdqbJ6aPvs0AB3j;Wv}jd z*Mr4(HR!28Z;vJDoJ}=Lh)(ag3i?@4?sN(HVK|#lTmL?V-I(?FD}+3H$c3$uEHzKr zb%_7g+(LTsaL0>%B;7pBRO?sZ<=SPy27F=I=66)ys7{s}B-wa`XNl83%# zU0QT!02%kL>B~TdousplcB&Ko`mO?o@-6_s(Q6#KC;f$pU!u47pN{*zgyK1ThF?h9 zT0y+|PEhsx7Yiv5ZBDK(M=tc%M%Gtcu`XkLqSOnpwP)t?AM8|a5G}UJ=#!t-HX%4C zkNwmipUUPb5lODIU7r=`}T862r}A)JT@dOtr8XEbW@qs9{>vI$YsS!5(I$PSs2 zbiyP!kij;zdOV{s7tydf?uANBU*4EYzueHi-N!%mxk{lp6%XcJPjK{5OBnHeMd8q5qYX`xcHan}pjT^jDLG zUcHcR6YrMphe!v>Kmh$h&{4v5PXI)k;L;Qz;m&Aq5+#r{Onova-VL}(?@z}qOh<0< zBh*xu!3>z6c6eJ$?ZOSu`yd(U#zvX;C{Uq%F;jLm1|8MtO(I!!LRXZiH&1JhPKl1L zeaB8`p19vWi>6G%EJVPjWr)mIw+a~9HQH~B>sSsXEJ>dku;sQ>QnoXx3;gODaNm1IW-DT0atrqJxzkue zDB{i#WIH*Nh!ubNvT}Kz9_gm{?2g?PLl*wk|7be4EB+sPT4e2W_174Q0cUJ#5jL@V zC3?LkdRcQHF%ThrQQv`}QSXnGlvYvV(ZmIjNk_qjSqQn&Rv!36qr*>D+_zz*22fV1 z;Bv(EzdHic%dZW$TvDo7s1D`s2LKBy&_83QxBNeCd1w|i`uLl%x&)FIw1}HWxXf|7 za+0A;J}2l|shU>Ni?%R|NK&j@U|9i|6a%t}Cl+3OlRNGAfOd2~cetFmP6@5qAn*oB z!2OQ-elqQ>H2}J3;}apCI1Bp(AY_-W3(oLh_gI>U@kuPx-R z$AkffCPeFs(|r79vha3E`fh}|fB{Sg=@Fm2AIg*V<$@9!dB7>s7!@*>Ly#Az!SBJq zuVrOrJim8MEN^=vZnS{(Jx4^N1JT|cK%>UcL5JtX4TM>Y`^EEd}jC_MY#yV#1ArAlu!5^sna-J)c(K%u>|i+{Ed6=dQ^ z5i|K~iF;7;WJABtXz<|o4FIMghbJdkRrwU#nII@#=D-K$J$@;{h>s|30n2DbBI%1( zwSHp~`Hx{o4I^>}(SMmtE=kgYg)L5mP_i&a712IfIr+UD_y|Tv61j1?sJl4R4+3d{ zf4;2rj+v_Wuc-Z5jfeY0HP?{POQIauC5IO$6pH#;(m)xK=7|+&~yFD#160Q{aT=#U?{yK_{{yM67TJ6C4u?8s{(KKB%@kQ zRh7Z^F7dMQ$)%?B7@jOxU!USJjo$-zLlE56$iFiLn6XPzbUJJxB}x;z+UoCfz{Xa0TTn|MMGSR0?vy^=&b(TY*s+I=f|_~QyPEv7q7X1lXWR24!vApf zknbEn?V*!(Y|tmK7%HqVe=@AR>#i~${U_^%=z_V)pX$rZisU_UMRTYUu|Y=~-z%iZ z6g=m-*@s23Es`=a04V%UCwwq)&0w(1L~Q4nW6l>2#wS4RRQvhB_%dSI_?@_=Wx0xO z7|p%+HtqEeJ2{;{wh&2HA`RVpvj3h^OW@9UEoW_I%b~H!Q@!Vr|K`m3!!i8d6nr$z z+T65%x!^|d!oZ0CrF)18`HCPEiBFjS8gSF!mA^WD=n(A zGc%rtUQzuobMD=gA@>(cuGr@?^~c}j_|aymZ@ZRomMHjA-!GN$zJpKpjoH!q+|aW2 z8YPs1%+7u)UJJStbJ9?y_h6k3)TQtlz7}&BctZUd9f-TGB_$j99oe4l00pPZ+X+KO zZn=smNrivYK!jt|U^Kv$T5=xC!f2fS>(RHRlVA`Y(gi&}5^ormqdRR=@YWUFd~EwT zDy|~(FPSgZ9KJOEyL+ec?Zmow{kluvV@!0dbRBU$K2K>TY3R}NeIKjYx6t`h@AjbG zFL7n4dVVt-yw;~c4$4DAwz;y&AX!r)#26@9*YCfmLAz_|bEI2P=E!tp^&=p@V}%!% zKJUavOlyGn$i=b#{P9n% z+Z#Gk8s5hHvo*SwITM-MSB}ue1{_AS4=A+Bb?i0I{5pITreFfqKjrp1QHx{hGT8Ta zg=6YjLaL`h%?TX~joQ3wy3u2JpEOKA%Sl1eb0f#z>9pzcvJ7Lvi&5vNKgC?kBN+N- z$!96ya|qnh>xo~dc!~S<%+=i8atOky=(7aWC8f@>`6TX~;Pa3yoe1_gImFJ}<(s^z$J{f}F}$T6Kcsr%}my zxJ*woP~6>u-H1CCyAJb{vT|k*bZ@^1{3f$X@uPl?DWX*?dA`p^mllcmYo(f1VX za`V_lTC?0IMD{_gO6QNl{%DTJRMRsvbid^i2ATbekLOeOXcR1IvyTN>ZD2z@y}R2< zK+5W`FY~;qyjYG@)lGEUwteP!W*}D6mXHo4%IL2pv6Czmf>UAY5+zktPNDLq-LHjm z!{YLA6M_u+oHpusxJmw7@{~M!!}Husa#N4a{P?_Xl@wpfJj#~9t%xodGDc+WB$+&U zw+C@oE5{%I+nwx;0h8GR{HFj2m$7$}=o`{_r+174qynTh=#l&ugFrgumVV=$d(U73 zQ9+aBtIwJlCsEXUzXCkYi=n8m{@mdazvC22mT^8dCRy z{Gsph@!z0mS;9T8a`2cme^>bk z^iNCpAT3Gwg>%L)kP!OV^T@I~L{HMx z!(%BEZ7krqxcHQKneXaLXUOPJRG68vC~@0@&!(%E{kGi?=FY^Qe=}V8FZfb<4xJop z6Yh49+|cy?iWBI>5pehp>A0^NMx%KXIYfXOKfNMhP*gI8*;}exDMgyq0^Y) zJrFl>V zG%Wm}T@4@a8;p2?BB1`;nfXI}XXuX(kuAWeldsD+4;uKVSI!Q1jPZ@gD{>Vm9c?gO z=+>E_-|ZCKUu-L4j{v83)C3YG24h55slm)*$LUgISksH>W1K7|(-D(^W=3sou=s^* zirg>07ps#+6q8_1znKcM`f8sjQe)lA8wP}(Fe+$0>6%iYTx*MO;Vkw3UHO5M@5V&9 zZC`X5{gQ4%L6l&dSts>+!gD!mBtf@3Qq*dl4cB;BOzwoeb6c<9ZwXiagM*KjfkGD( zNtRg&Y@>~T=I8s~^fz?Iunu?v+?<#99b?rr7xt-~tMoAmo@GJ1z>?E@Qx%BDhqibe zyP~clGvoAxZpRr}d5%TPAK%$QdlEMBJ8SvQHudKu@0+w42=5Zr*6&7u+Fh()-zxXnt#*Wb_h%Wy zqgeM>xFwedbY?qfEdDMRT*027J8SrAGCOXU65;S9objZ;iB*-$HXh0jvDR``3Mn6x zb>$!um}%#{n|1;LH%RTzC-N6QBwQa-F0U9b;O6p+xUy-;4kR)(=)^M8&B)#8ak2C{ zJb?e`Z+MOTVY|rA!V8>|69-aMj8InDvLLcLR4-ds2K_q3S$dLe_tkAkxTVR&2dF<< z0#&u}V2P9cWJ6y`t#7Z&d7~itX1^B5P;+}bB0Pz7BJnu z?*Dyi`>bhIOW8iNLfEoHgCbWb-=MU{-o9Va6Fsww!dn zKmEJAjNpY0Y^T%TQ97e{-X?{nmB-;JgVsI#utEahwoF;~o#8^yz&j#51?F1T*;1TA zQLHjG02<)-=+I8j@TBReND3DL4j^BjpwM8DqBZ!)uxKLGwdJuVCbZaCSt;;!gMgvg zm-ow5uO`&Z}v_n!Df$FP8`=kk#Vzkl!0S@@MMacT}r5bJyP)}J;`rzpqF_~W0x}_ znxB+Vyk7@m7)!{`fuO{=wYmOEr(zl*y3Z6%CcBNwAZmG><>=<+U-n$87L;8ZvKS$@ zBorF#0uPggW+-SAgWU}BWtaG0y>(-4#Gzr5!t5WJdCRMas4tY1!^KLN*X4Y)!P3u~ z^8f0r)piH`IN?@QUX0(D&tgL+KU4tkvxLYNk6)oJYZZ^+dRUiE>`LoCulxxu*4x5Z zP6SIVOrJQ}N7*n@Fv`Kx0DG3DZf7-IXEHU`S$HB)On1XprY3^jr$~$+`$-=`lncA# zq$R?=3~QEjR%DhEb1XV9li!8003 zg}uMdnRxreyDpT5@l`rWko7ydu)mXSi#Nk75Y;evB zk_Eyt{J1P^MMOon+uO>0L2Vm+xH0ZxiLe2a*_U%+h3D$EQX2SpeVhGUFrtGVuc-aM zWkth*Lg?efX|)}8n<(efft9{XSI-X`c*2`8y-0xte03An zn0fHvbXCEd_CNh1w4?e!U7d@L&iv>IjMgE4jJot{&Zju8UWFt*JUo;+^?U)iiIBdQ zNXg)!Z)T3I4V-_&Gw|<60D)GJ3DPN;GLQzk5%^U27oKVs$<@?nudGmi{EM81B?CZLwHG0c z2ihY>5!6k8?k@h@K|Cw z={;|McWL_DOoeYj^KLX;%BCj%Q}iFAno%+N#@>X};hdVBi>0FBo>}}^!dc?kxun-F zPJ*tgz1YCVlEu4IulIg#gFG*AaOx>$@WVp7LlQy5Zv6(&UN!BwUpfN1SL@2tx#R7( zEJDYyQ9laX!&y;9JkD za=ANF{5ZS9QQwSvvcIMUjj|??3RmcG1z9`MT>q2ZP<&C05dhfcD*t_6B*28tl27;r|8-MgIa;Fu-_W z?0f=q4XC|5lstWMFC|WeVd@`-EX|GWtx@3#3d2EKVl}lD9_UPfbrYS}?Ufd!iGS1#WMR1t?#%EqVk|IxF!^I+{1-4 z6S*t*&{%FXWnF3U%kP3pt@yPJnojk76A8D7Bw*)*n0GX_N}YZ1Vma|oz;zpyfg?ue zI>3H*;6c>)=nK?if-U^b{#S3E>58P)nBAWR58r1DBYX_|L>ao(s6;irDvhPMxV!PH zULj|(CMQR)mYxefmXi71c_%`ge}q2JOz#H?!oOVP1E`I5BJ2W<=2He!O`kP7zlxW& zO4_&#JR~ZICPiwY-KLuRc0E}BqX@ypnsd{#qCjJmk`g~7Bf;4tq(e5<&~T3nC9o?m z3&s75!WlG*5BF-vw+T_5J5$eac8Q3Qj<}v1lz4^%dpi9N3A~s#C?_}JdRm)vD2T09 z*~;BfrAmM?&d}wKNUGE@wygKL+{J|&{RoUww$hIi^*qLtq#pDdzZf8~Q!9(}7VCbL z&-I`K_L{7(m41a`d|HMPD@8MVE1}0jZ6P=M?Xx1&@Ebv?E$ZirDyU&#wv{lTg$}$a z`f!SX9stMP`B?nwY47jfE98#XRZNBE-Y{6f1Hsm5QMtXI6{^qTxD`#T1`12Nn8x{q}D9rmQj0{!%_!boUe9UgnQy1^#6YsBmB$(f0OTHCn{YlFfCT09P=iF=u5toj9eDZTDMHnkp zC1GSF)XWs`&z-}aB=N-xN-ipR;rVu%BOUtbcJ&9U8+&1(A@1+Mt~XWD?wquLXJ=N` z>L|G5kFTIM0R&ACQh^>rGu6L#f)t}6wFvhSPj$OA7z*m+u~)RYyCJu$y=wh;i|h1W z1uA3+f6R7lT=A6u-S7r*D1iX*XVFy;FsiNKn59(Fv|9RPHh#Rg+>di{uj+3A%Ivt2 zwzQZhJVc-o?2;bG@pQXbAuc#lO&0zD=dP4`BAwBzkgF$=Y#YA>f;-;^*tCQish%6g zYznG4Qsex#4Ah4XRGSuHjtQGMij^U~J{%c*w6otdUUo8Lbl6r4hPOdV z{#Kx2Tee*>#^tGa7UV|Wb_~yjq|A60GlkH_xO}kuMWsHMuiSTm)=4@;QcIlTLlxDPYlRB@QDZc|_duPRq_6w>5cxkFY z+}}7&dPdqciev|JfpEN24U4V)00PQ&*kYj-`RP4^4D+jh#yBj|oTN$TnU|nXeypouc zV}t`}JM{8CfXOa}0mpv82Y9Frg2w=X$Q0eI$WB*~6Xq%rw0160U5wqJ{3aHg80!E( z04vi7-QKoB7}WPuU6O%w1n3$z_Vi$XR5`PY zb6al$3;8`IYFE59+UmsY;W-Ey2KOZS&HPb!z$o>3HGV^QM*3ANf<`& z*9h^0H%`1D?hZK;VE-0_L|@w?FBO|cSWRzEqp9K3xT{kzf<9i`dPLR_;RBEYUR2m5 zuR@NDG;p8)F6u#o{#Zgz<<|A!in8h2(JDCc7P6#Jf=xpJHtHSlYg7M_Ez(&!@$ybl zujJ*QO|*soA4cRCvydL)`3(i9X1twX?pR)K9&Ss?p9S_v+cnv(9eBoAQ*Qv4t_Yvx zR@K3FiIDP&fl<8LFv%wftm5qx#b(?IT9T;q_f?3~adP3*i}SgwWGa3hAhg%~k`2~~ zrUD7Dp0RRGt{&z>oQmqYZWFotNW)%0%ztgo@Uq4P+-;O06cgU;T)!pF#<34Jn9=*> z1P6?LGhWHx@{&CKawqu`jUs|%N+>;VN-jTIdYUg!{z^j#M0+LqfRh1cF>u9n)JA4n z9)ghY5_vjpP%8TH^s1$e+TjfA>FSjR`4EI_BlP5we0cizi%rS#ZVZ^JYQN=tIFBu%oOznne!JjT-Hx1bHQed$z=GvI!QU#+)(>|YPOguP@cr;$wtFZr zNkjfcGaMp5%li)lbpmAixRkU8(9(b3Ai*e<=y55Sasi%REcYQVka^F3yu8bC-~Ohr z1%lygfG&s<^!OmsXJ>Bv0MI?$OomqMfL5Y`03xt?stj2o>rt`EOY zF}L6}L2# z_KgW6fJs7dR`m45)8xBSrQL6)DaIG=+y>DpN z*gn~@rg^xO-ZynYRgm1{Q(!GhV3M=C>va8q-+O!>ZQP*T`O+H`*L^pyzf8){FuhZl zyKJ9{(5VImIWLLoW!vdWwGYx?*z>=-lV6Tpzr0o#H_Mz(bc+$ctxNquo7v125{d+m zZy$y?mpHY4s%w2bY-+WyHh9laCN8-ESw7AE`2^Dx!BcW`A! zfD#UFhY{cD>BYslmA48cYUD-7<12}4QyKM~7KCsc8g51FuqsS~$R-nUozuYqAYy)} zU;A>Xz=-y0C$~Q{@W)Bexg)8eN-9qkHHx=A!z9|S1=n!;%&V!Z*ZzYv;{P~(&2uI zkpX>WsYG5YR-latWe@ylI+>%_<-MSLLMsBYeikG=P*Ozf?q%HDh|571_OdG5p)LKG z7#DtSM(oR4GS4{uZgrB6JoUSxQBOgX4_LR*oVL>((KZBA%6YV&wKQlk zxu3`L!>5j~yMwQ1yam=GS%S6~##an;M8Im@9-V^Ue$H;Y?gKiX#Lo0e_(q=j{ot?e ze#iIUN@=EH_ybY>jp>~QQX;eY2au~rn%-R1O_*i*xVmhR8_=h!2avr`61O6Zo=Npd zouRiIE(MXy&g=8B?_#$a4AOnRY2B&Swe4N>XPEBq@Ib+#e*1bEjpjpBJ%7xCl-3U^ z<1%4H_kRPl+PzJB^ekpa^#h6JLDjFIupM@zqk!eKm=5fCMrsoD!q3| zD1y>OkRl!Fy(29MNbg85p-3;G2M9^N;PbrS`RDhY^?qlatOc39GBfv{yIl7*d-mQ_ zzY@1Ld3Kq~(RgaX$W0E)EFAfxZ`mQW>w;fMOtg{wxk)hm7i2+Bx!8i&7ZetKVrVkf z8cq+U6#gdc>N7aJgfNdqg%h>#S1IuvqRwf`sE$h_<)_T73OPY01{xIK)#laj0&E%B z*HsUF#22yjMmckWvQa;IjS@#*M;yW*B=ZZv1{Qp$_8bI#HZ&FG6K#CrNB7dyjM!a+ z=M--2w@!~Bxw#sVa@dc0lSV&g7hI8S@;bNJ)XqY>z=`8M#-``~2Z|{9C(oZUts9V)&X@WZWumFISQ^hZd47Iz<7s3mlMQ0gyBy>8WSHP0@R`$-Y;M8Y* zlg9hEC=X2`*k?CU_JJjc7VE6s_`2$L__JKy2h^>6@GaP#5GsfkJ_XxMs9s$>v+r-goli@z1lb95ODdn3|U(q*VHwc>h zo;cpuZ|t+Rv}<$@c04sBeZYuq`<@l^o|hK$Xb2q1DQB-Zs#$ralEGgDH>PZRJPzVr za_1?V+eN}Uxo<08&EGuE?my@hEhreaTdWyXttr9xI?SE&3sV4{?9vKH8`LJ)$5A>K zuj>}RZkxQr-qZi}frl~vDFik7fLH5n0Z5DSu-V%LpOLPbADY5oV1Bed-ByU(QvO}$ zNFqj>n?>P`9jUo?&h1?*$S+X?7VXzIk3RK5Lz&a&VsD1K1yh33EQ~HTwEx&*u70G~ znjrCb5~@mLC;^37sYad`LXYp3uJ|Ni@*X2#p{K4T_U2FbB6qc6IPm^dK&4eVW#weO z+qSn_r{ox3WD=3^2Yy%SSIdsJn-U1h@lNcgQt&wbz0Psbr^!$u=h(2kjI^b0Hc9VF z4t71($L+<%TV}8JjTzra0GL`_Wk6+tCt|;N{gaYC^;{($fMb-$iWl+|o< zp;rf-in?GFPlw&x1y0UrUfEbVrtgrL+c=7dr@J_5nVhJtg>0(^W-A%!q@*xnZKn*^ zc+Q>ai4#PUkyc?!DYKd`Xa`>3dqP=R2|*XwL~o)MtG$Z%MHpAy>o=~}0P3B=w*v}d z+DJ?mi%sx@CkelDe%UpKo7WRdE0Yq8>RX*0bVaAh`zSz{Bn37mNNJJ~C;wc)kJ) z8*hL%+h61|J(NX~s~5oa>Llj&Aq`|^Zla@C;p($I1t(Pm9_(6S!5KWFKTjB4FC)*l zsK?)1*1(AFRi93^0UI3B^rOl`y(T?lB`J?7LmOiF@;q<6*4*#%85?TjI%Fzj0^{O_ zk4#H~E2FaRYm3)({<{a=wtd#6J!hdb7B6lNXmzLi2Gy`p$R`8n35VKL)}7evSfeaF z+w`nIS3gP)v!wq#_KRWIT~cuc*#vKeZ`^-sMyVZPKrKMbqLp~Ndq+)MtGg_aB`Zx+ z0&(XiSlayKZW{*8(S%%t<8JVm8C6{3y_eeGJzg3F(lYyAT%*VRhlH<%^zm=nK-$re zN7EC$+bB{ovR7rzovIg+2l`p7!!k_t!g0>(*!jn^4I=Z*1y$NuCjN62HQ1FU06c64 zMo?JY9)9xbiXR8!LZ8zBZZ|BBUJ-W;(mh$+-G`ce3Y-Z2OCPUvtVcpqv*n379CQJl`x-iMJgLA+)rQ2 zt!OzW{mjRxOF#aL(0%vh;$}zv8N{nilu#}4{@Ln}H}T!!ey`SFM1M0R$chsB`a7EJ zs`(YDHH7+mD7p_OX!q2KCmI`vA!1_ z;7B<6EO`-zL-5<6s)TocZl?YU#+GQCs}+*&#@$XP<;B?WLBGYaKctv1nM^LD+j*@C zM$B@8Q-VQ={EL}CO`o1*jh?%9@=_^p->QL*ndcL{?A;IKV*5Qdz39>n^9ak{An80gbO4{eM6k9bE^6o)`t2tRY<98dXHnXJlrCmj!kBQjIj;-` zh+j}9KYmg&qelM)lM%|C?bWJi-#M&kG1EvnNKFB`>jFsHz?Q1nWTZ!N`ODJMUGxK! z{GJ7>__stO6W<6g(Kk=M%kj}}&>u4GxRibrN&N)nIxfoWby_QVbw?hY3Hl~!eJ=TK z7^9`1cq8giG+khgrV=nZuvSoE=_tYm;6r~H)+YWa4(v#JbeEW`i}+57f(Tc~8+|?s zxAv9V%ctM`1BY5X0gb*lHrl3JQ*(de(I(OJZS$vfWoSR;v-$GVa7KMFo#k$y3X;Jt z?UxV94$0Nkl#*P0JmwET{cWy-{Q0P&B6kVa<*5x6ZEHK&gm_jv$hjd0UkH3vuN*<| zMS7JL#^J6?weKqTT(o8L`Q`)55FvIpXzb3$&#%>I1-Qxc6a~oSEQqj`H}379sT}L~ zGh@HRLG)3u+mI~hG+)a+~25+?;+N9Gu?%_LHv&FwyjRy)}%5h|O1GxKW{eaF963*o^Y< zSE5x`P{RU?e;vc^^*r|x~AB<_SXXa7(xw?f-8fL?1t(nS0XWWTJXRn8h z8L2Mm#A|&Mo>9my@z}re;m54{)4V+x(@;VT7ofefL^&^7-QppN6alOkVIHtdhASXO zKR|MK*82UF6T!-u1V zW5MR#+Mau2WB4f*CelBRBrv%!iE@+g`{#xs|kwDuf_0zVolHAY~0F zajv9Ex_g3=j$zQQqyYw`)evap^t&%8PM}!u0V@lNM)*n~?o^KU6jg7EW`xv-RmjK4 z=l2*>La8l@bmI)+LC;-MSP0pV%)DBdk#JGX#)C=mrp*NxtapV+hClM0f$NtrNWD9V z@OXZ`+`$bVgzr>HarUXZgi;5GgUl&Rn-SbkJke<=^y$`>woBw{Duma3Qz#kUT>%Ey zSBH{BR-em0U4;xN|Namfc#m*>WtqXtky`-raquWuA%D5$MLdPZCmXNWOR&#w1L}NL zGxmnp!Y~g6#WpYrHGPnIB*|ntYw+?unf)_i3n`m~(aEXZKgwWY!ooUE!4+_%sueT-aGVeN7l3%f6tBU# z!j!$;SMA`k$qT}__>IZFfi-*W{_&bQ-rG{SSeNvhrzY~Bp9Qh1_09Y-Zj-il1n*Hi za1GvN(BMTHyyUB2bgNocm1t>12iR~FC#ijEXPiqtzZK40iTwe{x|t`ytLGw^cVYi4 zX(D-aQ#+3^=|&GIi;fAR$T&XD{c=y!?Oi!IXh#WmXT}i&J|}q^iC%C&hsp8Phn+Nb zuf9fn($l>8Q7s+ANLK<%e~-@Mh?awuVuy{nCQcETc=QLmOZOL3pTIBEKlEZnWhxhv0tQH=3Mgk9OnFnm+G~QEu7YtI;@7;YVRhhbLdxj1$tU< zN8Nt)TacK7>B6-U4o*6R=UlteQm8Jg-X3KIg`2bhgrWnZrVU!zQKWf6n4+{$}C}rAtW^N z%8>5}d+Th=;U%gcNG))m*6D*V5W+4Ee0i7iLOs`k<6+LeS<;g(nZQ+^*$Rz@7%Emb z1PmKE7uak5`Q8{J6w5D#^8ha_ zCN4)VfUdZ@Q%yp1UiG3(hs$$2pEhX7O3g@qnEr7dU8dHnCwm&9;tTH&GchD&&LMF(c(8rFmgUF zZ5@n1vbeYd7?uf#Adj) zZ!VqtSd9`;r9{(reonh>_F}RgN&z1Pe3Qz2DTb4HwP?EBng%l6S#k*>+2PS+-SU0> z88&_&@H6Uk7pgNrFwMK%T|yU2C_DA;K}Fv}UV$2?tQJT}E?J#Agyi`}{xB&s3jR(m z@Yg1cEBY#TPmoB#`r+M7x*ekV09pim*=GmOK&)6cAD+%h{J9blt*5ktpUO47bSX9T zd8$%_?;8EYcxkB{@f)-cFSovTl#NAA)vsV&Y^lgunyodUtcr?B^PeHTyNXgQJ?#5k zGnEumJeBFcm~=iBqp$NZHS<7lK0tMs(WJUH1R1@&1}s5UI7G<9>9z=ogM_@fuw}oc z4@4-yL(v<{1iR5We(AyqMl{rll+ckU>QHT0bG#vwN*Ck~h1gW9d37(iG)X%}^TtM(xDNx@{FEXB<8>~yExZz8>ldm`DWR&`eG?Gk8 zDLnittr0*VS=NYu?noY%nd+0#A@j=Y1sJ-6G%WXX5|+8+@{EMIJ%n|YWeDSUXGhI6 zL)CeK@&x7`Amp%{K=;u%P6qFO zc!_EAU|^Q06N^?X;}Fz%B_H|07g;F@Z7i32mhex%Nu%Ouu z9J}<@vIRX;lsHwjsk;QjEf4Zl5dPtESiG|N7`etXy1OV}j$ z0SzTo(?Ddl8XwfIk~lz<#LmW;Yn>U3bJMV+^6y1gvg*TF$p(=l$PFund%73jsc14QE9@nX6a*sc&pLK3vp<&&ftQ@S-el`dqOv8RU$E-80G z;|AXQnxzeb#Y+ar5_`1MNNvXz8jBrCni|H-C=iLi)S?a%PF_hYI|A?DaL!PDK7nwW zhij@Tj$d^Gh`;!SLY0A~UXFCBfsRX;-CiwHmv!v8n_zI-e$=6jk*gCV8%2dxBx31& zFCb0}VN*OWb~D7T_4MV9U>i4Gd+S?9ME!Cj8=_jkq>+L)#ZkRY?6tnZ9YRG#YgT30 z`jgR^l0|J6+?(8{yAdEB_%tlCH>?7kohUdSgPaX&K0xo5lzYK7lZ+(RQyxdtwSZ% zpgEzH0zdcY^jMrK?0&hwIQcxHl2o0$p-vrYb){D41;63HN4NWH!*Fa0D}T1qz4bH` zm$q?URbfR!U`FY-+$X*4Yu|iuFHx7R{8yutyMpvR64LgH_$!yff(%6U-3`MMtNT;f zSAQ}kLHUYrF+q#~(@q&eJ-yCZzLgl&2A)&gBKe_y9m_+)mjW*r zIGo5M_|ib{&rMGtp)K|0F>?2m!PjbTtOcrob;O-$CYk5qgkej*_mtplQFjM>NvK1L z4h5CD8dkS4B4;-~_+hA!z7_Hv=qwqO&X0iWwm!AMSG2KIwLlLcZ67zuGONhe<{>@k zx^6vGhKL})4`b8Gp>|%?2m1npAIhVpwdKJ4nSsJ_RjAEYVTx4`%mVIQ0 zj`by@zl6eYPRb}LN?4+ALg|obVn`BZB5L%7LT`bJE0Y;$n!R?83O}FGLES#+BBWyCTF<4=)!Y7pakg$FIWqzet->?^w<+hL<~bx=T;mB|ahB_(e=iE4Jc! zPukeVs%EAn3cl1xfm$XkiPrs%KvJ_2Gx_5(fe=k2;oC}q_T{d!z}f~FA@W;^zmU9w zZzrxX=ZfI>p}AnMb>p9+!$ufSX9zK}d)zzVC6WwB2|wxZK2hO}=qQtwA2>+y0pheq zXbkJ(whwWQq~SfGc`03q_XuAFbsqZyuTvLFxZhj4TW)A{A3N?y*Mg{KO2m>k+q0G2?jwPg^hy&#SQ%jHg3BG_bV`@ls8k0zT4IY*ado z(&~c@$M_nkk;hBaT82*ilai?O&sncHzr76-O0otUow}faF=lcxR<7Xm=ad~XkrVXw zCln`d7zlW{6@EU~QT{2$rBLcB`$YDBuaTAL$vCF2zfrl}uDQEQeA=jP_UPmA#7X8vR1IK--RYqx?{A+dIlmaN^WI>iIndwpd zFYt>iBpSZgR@BCTvu_qQS?H8NZ9vSDiN;SCKodO%9B|bR&F2_ghCYbY`jSD}c8~u| z);oQpzxRMlLmYrhg$}ivOJmS zxJIv#LoFWah)Sp1w;VQB3^E*$C2V@pD1PVMm8G?Hf|Cg@qsXJ4N}3yLLPw+iyM7-o zqx`^csl+f%hmNSb7<;z#hVR8KE5wGh<7#F^@;gK5 z_NBjDcLC!6glkWD=TYiw#YiWqzr5Ib(z=Mg$VNduO4!aNeLz?hlrvU(A4Yszhkgrn zQ=uhj)aRLh_{rbih0_B0L`}l{3)w|Y;8QDc676(u06VEs5m)-4$96y6KzYnBMnoQ8 z%#c-DiRLg?QmwfveYTLd{p_EAx`uolH!OCL;dD;P2581m?+8UAXXJGhvJl`e(I*!F z2=HbN1ip0sHk#?2Vfq$d?Cgcpu&HvYk<`?k@Wj$;S6HPAt_gqi_$yz}+YqLtAj>AlO7UM&&tv=` zR_)fQR95$^z_z1)uW!zT!QV8I9PZ}ODu4hxFT#&b=$5BG9Vwj#C`9ePhm@8p(mA;K*bP@!05gPfYv|ZVvX%~|czMhE;PuTlvLIG~BID?6VAkV=3Qc)J1xo-?G2!bf zq6eSPwb&7Bd+rg!Bv}nke6124`)E4j44O}jABH~!b*`I!44@k{32SfC3Yjw9bKg5l zhpiw+9gL9~W`7(`0vKB9Wl18pnU*sui#35uv*Pqv_MmHo2r-o=?4s7437QtlUH?gS zHhj><3U*-*7!p$x_5wi>gsN?`^tP-3zm}P8a?;P~it^mLx}Rq0m9P(853LT5ZCC;P zjbt`H1H&l(#eAg^)7J>V?-vs+ZCG20i7f4?eQ)tSba-UTb#4q%dC3)t5F}=^?-I6B zvUcgZ@yE&dIY{qNU+6$u^!tyVC954bD&1|~eeXUvYogF@o^>Im=IN@fGJov-Sp`KOiWwq3$MMG5?hJ|SB3Njm066Dg00055NB|%v!hJ#5_=9nkvY$Ry)b*O(Z9#gnJWb4FB8j~z zcMqY)S1yvVv$Zd6wSmPtZ|n{Yomllf+)ARY_iAb0M z10Q!5WodBzUVoN;i~dymrwD-J|62e!{igt+{7;JFF{|g%XUn2i^di`Iu{yUNXU9JDEDgOtN|AWE*hg$!o_xvCG z*#98%|LTeTzn%SmIe%Nea`_a$O0(Pd9nw`^IP8A^O3ao2UN?Wt`EisF`pH!7+tN5q zHUOZFX2a144*-B79S#_B7*Fsc&sK`Cs=n=zLd z`cvd#1ri};LDUQb0H}NZwmS@kjGAv+sl3zn-E9*W?-2IHi{CoDVl+GKU_mRNW+P@XM*&UB3$45%`fRsQ`#VCl|xMg|kmdPqhA)lr)(Q<%bpOFogxD zVG8Fcqq;qd?gckHc-cnxmO(3<)ULN7>J@rp#<@G%cdNOMXX$umSf25O>CU%niA=bE z04^5nFMt^sY%KQMYs&W67gEE;$%K};YY)-UN6p;5^5O&62`3mQM(-WErFMnjI9Gdm z;@R9x-M*N$xD<#;8o1pBDL4Hcb)hlaXM$W6^RPuZPYCVp;2Og14QHJ7|HeZbiO}Qb@_lk^5ffXYDUjN&`iy#RGo#RkNDro? zUN6raOeCA;kL7@_p)_YMNZ+t#zL{LQijTwDwE3Pl?aYP0Mqmn8rd%7%$R7f!f%9_J z-EWa-^G<#j*eXhP#yiSh@=tyBXZE?i46CW!ZcntIkAvH7Yk2)-0Gn@&FGpj@YEp(` zC&9l~Yvb58vG(5ffbh!>j34X2vGHHth)DT4KU5acRmXDGdyw9jeF0(xi01_KvEf;5 zQ{vuh@?ys9%f&G>D_}1Lr#%cuP$Ry{R18uYAd(~R33zM<7X@rnLhU8bk2lzy0N)X- z(a@6?8H~7W`lndg2bYu-l3gfn@-3MJrtfBkU+Os8vv8{U>+~A_6p%0Pd|%j{goTIC z4(DxN>3;nRPQHCqml28BdK(`gQatCl_I|0gG=FJo{P>s{ZEjhkX&xO|7}_0{08qDt zJBo}0&XeMkX)|BA60`CG3#7?BM8gzCPGl)!csgL?vHr2LcYN@1TTFl992S_4d+~F~ zG2gZD;m>L^kKN-2lqRSJbOvI z;;ImPl)A^_20Xky2UV2lP5fZ z+86;L%)h>rs$mtR`c!{LqIQD6s?H0&>GRwjoO{9_4)+8!XoIaba4&n*CRJ(yp z`*L`BeY>qiyYKj7fTN7s&TFPN^?m9m+<*WFnMloef33&O4kwAKPDD4ucB%Tk7(wPe zNHu^B5jHQ8^t(&o14q?{giq9G+YhJ>RTZY}_+BW?m4uk^yNfv)1|IppC;nY4PProw z#mB|?b-r!Os=ca}!CYQxTW)W)AY)tM<)cVl|A}?MnSk&SetSqSZeLTw3PH_rs$^Qu z*S+Ordu&=u$W`Y*LOEEeH-KNn(Sgo}s=H$Dx2>spOFX4}WbrqT7f)>^n^Tu+6KQ#? zml8ON|13Q(6Fw-jAb9giec`aGoY1nRhDa4BehK#n%^|(~a5rbw@bQ&FE@X22n0ugp zNhP4DAMDu8VCOp;Jv{dcweipfoKV}cW1;f41>yfC4l*vE8gN7je2%)PV?Fpmy3aa& zs2)VeBDm{%IaW8k=X%**C-`L%nhM+}{>TLCqn6T03^aJ`G!4Jq9{6tvV|aNUiN)Pa zMn1`XyIR}g0L#eBm|gScgJok}lb>XeV=ZL!d$8WJ3~YeeVO9VKUumnreWd*QZ%^-L zDbr)03-&U8_@OWBY0LW?Z~QVIgP@TC(tur87UZ5Qs!tsBO7!zj_hF*xp;?xF%Qu8$ zRI$+t-J-VHtxzU}+`hoSXQ2|c8TK#%9C3R+d=;VFFQ{Z(i~4E1CzlOIwEK!240}ak z&0p1LY@fV{!j?>}3O0zE+@synKkC7TW$Q;^z9N2V2`(Q0KrTCZ!BrsMA;$=p{etKZ z*R>~5e#BUNRp-yi?Ny{s_O9cuP_^5fda;d0rEBj}z~q)_r_N#d|uH zYp9T*jSWBi(lPG_{(HQT7W&X{GqH;b zi#|@rQnSDxmTgd9HS8(p3jlyq>zW>eA(^6y1 zAQ1&n8RLnr7_T08a%&;&32(t%?JXDwf5@ z*Hy7xsBZ-vamkN9FL{fYIjgcSFN}r{8M~^sdB^Aug~)MZU(vtWOQbZ9sV^J_i4nDH zH)?;U1TLtnax$&9>>fTJrjFHU6P=Tf3xMv~qI4gSEC?S=ih>%sWPr!MYNOxj9?|0n zVCHoaYv*`5Wz0VMe#B!9%~)7}iVES-%$xL=$ZPp({24W;^Bh6-#vZa*z9Z$1E}B5jUU_RS8+#}zzP~D3 z%kVH$+Pl#0hHY+PtPBT@L$QP`8__25kvdIaUUX~0#6Cl<|MtnP)QU)Gm7-ZD>k21v zEiBk`^hVOD`@(ly*MS*$Ip!7Jg{D1$8B^e1v7bplyXGv=HeK;xc82?SgY6T}-d~rsHH9(R+0m z=&;oVn)M=NweSo-RqXT692=^ewax~04=2D;QR=Ktk_nUzhp^>Fpr(EC*I=ra5Zop$ zDn!_rVWcH5T?wz6>%iq3RESNlE~hZ3zFVa%S z1_oW>aOj7%1r8^vNoN*&F(wM+4FB}S(>1|P6wy+)8zb6cI8 z;DhY(UDc}qzMbS_R9GM654k~={}xHf%yb26Ak_Y)NMRXrNMpOoZzy=TURY($TW&sn|8uVs>4H8HxiBX*? zXTf%xN`G=~T-9m>-az~9G2!WpU^!wXia1yNY~x3&=OFDbwQ0%b(}Q7Pwzn>5{;BtP6j-c%>1?mB7j-hd;h`ySg_rn zIgEpwY+}gHYin*lLwdL%kJpPJmXq)_-1h# zoMC~C9Sys|rwt~#^tVU#0)@8GEevP%e`HI(W)#S`_3p0d!n)&CdswZb3Rp7jpvQ}M zYxa=&Z#>z1fCpXshWcd;nfBeXl-6^VD3fl{)RBMVnYv4^1#C__r`$qyMx%q`S!gpp zsf-ORb{qEM!)Eh)8do_lr~w2fZXYjyti5_3tDufUjpKgR`1n_1$1DP{9IngrJE zCg*M(4OMfv;+Hj97;^MM{gj*;F|+k>^c8)i8rZCxd1#a(E5b|ruVUq2Jsx_2LQbss zq(P*g`NL#s>08RM;rZvbf&QbdorI1XlB?|j_gGAZY%hn|N6)F=d{U6^Y;)8>bdvy_ zj!yAYvN0A)L~YvEsFu6u=uOc%hg?%+;d9Az<7qF4)i(uV`01Od899eFp&Yp_fRafMhBg=48z3q4B@nUdXo$=_Hiay!Q)uuvzwZB)ND!PP@0ld*&giyF<#6}|QVD|_G3E{ZwWUUq<_ z(nJ+vjCi!s;HmF#A50L=sysWqjFaywTH$@Ep`uEytyPI`f-fY#yp0|5>KaQ;e9$7o z)28o9U(nGO?a4ZwfbahJmg0Xj>_!I5OZk07>|P*pLMJZksM)efgiRHUu6;6z_alG*Hdh<=*uGohXhsY`3&tZ>yUcGp#kyuxlUhZ%+v>6S3n+`5 z_m;760)K)e0uD#3d~-#B9+c>;i0lO0vO8hBMu`)gdkJ_vILX%$^LZS#KtjzBFR+aEMKiThlzN4fx%fk#ShyuO58*>o`8)}S&w@gQ}+E6wDWf2=Ef*U6gl9mrjW?goRIP~vF-5uChhBI?(`4AGC( zm|m&gLs!=J$gSE}#h<}U=8lc#unG3C+V1`720F@c+o-p595$uR1#2Ma0Vtj<1jvo= zUJCv0DYCWjj7XZ;4KaV(gAI|zH!8Bv+#x71Fqz{(%+}c=*M^|JxBpdaKor)r6jkM> ztNi1uT`}QaRfOS|mwfH^3x06<9*m9eC6)_akj!PG^~@nK1q`?f#9e7wC*Y0Fq>CMC zIf!CGs_irS+jo!QdD(8%z}}fn3>WveeWO7j9*F%r?;!dV&FcP(s zRkZiI1{;@+tpnT6iLx&nAGl3#Ii1vCUP(=NvzUSb#1O3?3QT9VL^tvGpO$jf(KYWT z1EvgEOy;^m7nCeE!DI;<&@QD~h8kZ7PrXkH9uUW=fJJs@T;jwrXHLH(ag4CYA8|~} zz(?Gz7J?63u-Q{ccpX~Mu2>9WUp}jM;sSk`fR#eRUv&(JR8j#r^hKBa=fBV!@N;^y z+xbpGyFHB26LtL_4|li;ZvgxRKtFF0`IXUVSuz6y|HZX~t;zGmoPzVYC+-*TnZht}F zl5RX&VBrZ!MY>`8*-v2-o#(?8Xoq#;C!fn7ju~ssQ2|V^YVjVeAM8%-ThWHi&wWLw z&$_OH-F3b1$pK`)N+|pWRb)j5zD%p`u8S$8Q3(CV9dHj%iDO>oK|YfoESidA_7(Jw z2sbI1a!w#KmO{l?btkeBWp z0W5rh3QSl066v#EcB|S2HF&yGRiUVeZY#=D(@DMPVa^O5)73Gq<*b}zEkjM(?)m}p};#_ zTZMMM`A|9rP_tFHRYIkOzVeyA2NmSK26RDbJv5#$6bHxGQ>pf?f#$bf&s0Pn z_>zI#p!&Gn4rOyQ3eZ%RwtnLT#}`02?Cll7ph97jmPQ*HI@qSLhmaYZ&-p}Q=_D_Q zy$84AWC?YmuYZSyMe$APO4a zlKZ1w98U)SgISSXNcK|>z$(XI#0vpS9A(g(x8WnpyuNlJ|F~HN)>Bc6fZuQuo6p&_moS)7B1biv9lowC4iv&CxnJ--6h-uGqG zHQtZ4u_%fBp3e_AxbenvM$J$7#5{?5qA(aGM(ke`WZo_Me$qp;eKp5!ntbM=p_aa# zv3&)r+N<}ts#SG+6zl|dP$Gl*$QI%`g8|SNS+rUb=5Z4gM}@Nnl}k!*fDaf5f6sc) z){TL*kbne&lCWo575v%?-#R>0(-2;`g&CX?{B(#QrShU)x)ypVkhvXEx+v|zb-LX1 zxwGc9#7fB12be$PPPrRGcYeX9Vpli{aF!;{mu@QY?34&%c*vqagee)86~P!cO0?0+ zVt*sos5i;SK|LEy^CW>ve^u4?9v2%?knP-(6s*?7D@s?uK!01kE!_fawFpx*rAyyd zVnsO6$Y>(`DC#aQg94VnOc7hN9T^PX%au)~+k?keI=YU~$|587upM>anacHw38nRP z5Ajpwv~Nay42-|~{_&>}XUDY-4d`#H9~*P;%Ku zj{804KaH@jsWOf5;a2=L%2jZ2abfl7rOxEgGr~!GN}t`02|0zKo#<462WGI1qdf{U zf6jx82wdPPNJV|d*0lA~%esIO27*rE|sRu)$M(?_w?JJ zlj}1e`D^>Gu>;L}n3ug~QYAyiUD#}jiDToc4%a$E4^SJOETlggcGLr5jI*2>+?IRM znQ<8a05B@^aArt`S|k`wr2g5$jul*}UjjTzbf9!|^5Ye*wqXMa{{GOa>@co>%+kP& zMQvZToK?y$NsB%^tzU3_M7IoEevtGvw|UndEnx8;YpM{Ezo0cYL;Cw1AJAqJ1@+U9 zT#}kJa9dc!L6Y7c$MKHcxS1y zcn-7EmEJq0_H-B6!80pA%v|lBk{}sz?O9#nZsyKgDd+S~+;vRcwKM9jibdGe_gzFp z`n_Swq?2D?ZhlQz!3L*cf7TTp!z@;Jp^g)SZw2Tm-eVr)Nc)nZ7+&8kdjg*M(T7`H zxAITleCxheLvDw)A<`(!A`*Ug8UHvB7Omfr)8p=r?&B;$0 z)}y;U_J8b^PkZgnE!=XookRNmP{IrQM@=yfSHAqP=0&hM&8194R#D>BK+aXhjH^Q< z?b5S?$Jnow7iW8Rit$$0O2wrQc2T_Ww^q=1Az!Qe+02&Z4C4E8qNgFM1;Ss;-##hpzaXPBTJD#{+wP=y^V`RelB z5i;QO9M^r03~<&IR$(U`^yM-&##Umuw4tI@WO3b&f7R%xzeRtkM8K+?cAHW1^P+$8 zs$KQtWw-K2!N+}X?E}!4TUsO+z2`92qE}Cir+wCK=WulK7xtI}?l1*2D$ZMh-BhkakJtbLe&-&1YMEyQxvObF2vF}Ev1?OMq zrBa3s!N)NPFh<-0^sWraE&unc!xF8>DcEC=014eU!0NEym-V26OWNz4{w0oLq>N;& zF7%;euiD~J)6$+wfg9jE$$=*ewx5k)IFu2)XUD#85&aLLq+ayx>|kKcGD>Vfv{$J| zq$2FG;JsPchi3J7pl`xbT+3aoy}J`=yEt8$MloM;<@pk{MR-;q^X3 zNsOc&WbVvZ@_BhOr^)rD{p*mBe>@0zg@7|@F$=BD0E>JLoD#6I9{Z}Y$l$XMOiIr^ z4z}{s;faKZ`wJW$8~{Mq1OCj{{IpM84NA~dAlcO?)Slhz>eBe}beS$qs&)QmbB2PS2 z{VUnzZ5Flbi`>**FE}_q;jJHUWtK<|ey+D0@>?}@ADUfnJpt}E%Mh3N@C5Dw-+10& zu=D=bhVcs*{RgZ*M?zlTcc2KD3TX-GmVESb6`Ra*CAmg$aLmwt@bRf^L^MG*_PTpn z+RT^zq>L6u;|3R{vjm~fIFYSUzFA|S^My1L%?=YQA6b5?%^#wf{}Nvl&bPb`w3!1w z)M1LX7>_+~$s@WwY?lB^rKl^p5j*fPBG;odng&N;0UE)yOT4bL6I#*}<+BDp3+ull z>I_ffqzRF-5}Gh_kSHKdLScBr;d*`#!mSPO6C5lkx`!4HVN2?dno8XubA9L7uDXn! z%22_UgwrPuk{_>3PjJBc{{La=Dg&B)zxGBW2og$*2~vWDfCx+x1Ef0#B3%j+Qk#WJ zqtYo#$LNNkNC?u+q$FjGQQLsU^FIFm?-xD|cy>Scea>}V=Q`&eXXw?YGkFe6ce0rm z5Wlt@M^OE{8GI&Neot&%yJyY8oxZhtSFDarc=GzG|Ni#MUmEw0lrh6MKJK>jL#kM= zC?OvTyr$4@pNhRon|e$keg776A51$@bKegUYlp8~{N)|iDF zaEH|=^Q8u#n|D0o|2^t^0h{#OqbLzP$tnW^P-pLLtP&_uSic2}ThcSOQsG%<8KL>~ zPp0C4_%*YPjD~^S1^#pFsqDa?N@Cl8sZIlGtk}WF^~gerBty`HN1+!KxWvh_GTQ(SNjA0ATzO5y*q zo4o(mlsfsQzv&n4{P(~{@$2H~Boo-W_h7`M^+}wqMI?|r*+xG}_upgdf7$h|t+xL* zcZ&yY_ob^V&wHpzu*QS{>AwDLe_zA@D?72 zzIw0@Jk{;w%rYG~aCU+J5v8H+gkctAhzj(7IHPVP@@oe?#KXa)9Q`SBsChVNlwuO9 zSxnZYU)(#tP-u^dr(fi;?b=ofkmd|yXY&K)I;Mdv$2;pyeS3=VoElZ@KS-e3DHiSx zr+cC1G94sXi{NER-N7fiZh?#xd|hvp0Cfv`a|s7uZ){?CNnZAb{S*v@hnI?$2)-#J?#G! zLr01yEopuoM^(I`Cjr=Fb7m>wK0QHRi96v%n^a$=up=2D1ov>bs7S^IT$;t;X2|Es z>y=o4@9mhWcKHKq4*@s-pdJw__o>X9fDS2l`swLG4_Z)UIbX2Qzt!mo7Nmvms*?_b zm>)A1O~P@Ms~B^m*FyEwUvcPKD$2{2trL5CjCB5wXt#vWibF`m)l*rT?fB8Wzb7>! ztPxG50gi0nW4Re+$;hyUm&bHqDfF~ilY_5suW)Ho$e0avVA?#3{_HgIy*0DPG;J0c z08<-dj=I61LigYQPt~TX(`mIx_h_h=&!`Y!Xv?w=u&_G<{K~`OMfsxj3d}Gz z=8ox4FNLRlXT`)}O{i>ikt$u(;}lW$N2gCNm8D;YrBTNgKlZivrMAT?^`>g)ymt>YGo*v#}HdLl_l>6`QZ)h!|RxjrkdBQK+-xI#U?o z>5M`_^sC4tq%`tdJZ_4qsNLRA^b`3GeIx=!?WW}x{nLIj<;7v4cW%vT=QIp8T}8^1 z2Qf6S)p3~#r9~gDyAEmY^k5MCcix^}*q;_>?(ivMrOvphn%2?aC4&oMRQ%0VKtYI4 z%S_D@F1$6)jwmauI`dbG~x4@Ku5@DiDiiQf7$JnMxcC<%ePO?ze=xuT`~!xn{E;?*ab>s(XS;5K)Mt# zbo9qTl6~9YW+Ki>Da)-BtwFpADR-kZZ0#FuDEMvKGlvsHkKZWV>&Omv@QYv&HA|z{ z2cdeW7xE^L@9;oa-+FMsiAFcl4OJ?-YPTj9n2HnM%04p4V7o391B~lemJKL@;rQRK z@3~4Kjkk}UOb1P9Uh*mDhDAatnp;aiEmbz$F+uELFyVb$(VSXks(v!x6=7cV$2T@5 zjCU5GmEzeA4XNZ)-M&`QKh{I&MQ@N{@D`5;M(-IW)lQGsKtKm9q}c~)^pNqZ+>J8hC5M0?;9oeR5}gveidE@>Zrd%M8% z@gb&F-F3fylZvjv!M6^FW;xBq(P#BGf+ZbG9Y4RwfA&2Kvc&I>GaI+<4w1b()Do2& zv`_WXEXl5B?X$02*k5ga4#k+a zoSrQYB(y5d{`Jh<9%X_c&!k;AvWx7bdeF}h^;>7l>yc5k1}~s0HUNxLOm3L% zE7()>O#eX9&I>HnJ$-$F=?-+;=RchzDLS@>0^}WvU)kV!zUHU z_xJbrQNE?|TF&Q0*F|RM=1yWTYL1~j-NeA@+0p3}8TE6&f3pi^*zAV6Hh6Np@KHSA z{Q4VgKNq1(+r@)C5$6hdEzNW82BgH>13#td`en}N?fdudr>3XJOo=YzCAWq=ya#!~(7TilIqF@<73?8ZW$Aw#mO5arhFW!K zsYOWK>&SIOPhNPl*;_*8w)_A6>h%q6F!_^nSmeoEj9t)7AiDnZ8HDC+lkZI`-j9EN z7Bzp+)^Vr_{T_H;w{$3PmugLvKA|C*;qn`82j|54D*qwkFoc~DoSvnPO*F=$wA3*m zKqh)Z0JZmnl%l>hXC_yc4l_@jLt`xrP zHwo4k=pZq-9>AV6yak&|%%x}5ccQ$_F6^Ma0db5)3X%ZDMi+4|=0n@6rLWPT|4xOd zAt~qHU+mr37=+#mJh&M^%n}Xi+fFk@Q{HI3e!b{aNOCngs1_|AjGY`o&lhMPDqp|e ziBXn69GFH>#e6K_s5cfU`!^|46Tvi!b1>~szIywaEXJ0WtUhQ0E6bWY&H1=N*t;qH z&QV_HIaS3EJRR+{m6ba&2SY;xcb9>!J!2Kbl5OJ?nypGuplW8_XB-F#S3orqHYK6u zFzy9W)hkJWS8f+yX;Qd=-w=jrax9k^Rb-x zk9O?b!cuFjTeczb$kttx?8%^S-LBUY5#YWOy`9ARq=Vy``1jYGn-qga9*0l&pPioA z?(PjN9DDDi>@70~2c?4$!2I+sqRsqd^dYJ}Ld4v@JThqmV#zx63%YaR24wz+A4C;$ z`xzv7u05KQgI)Rbk^b80EB?;3N>tdnA-YsKW~4D~eQoVmV-8z?fB(`;Y8cAgymQ81 zo;E}jfyh(7&^tuTS2GrLrWH#_v87h;z4)1WdYBmVvQcv+uBWh^jSkY0ko-bKR0e;{ zQ>_oRzIY`a?MPbiM)T!V9j(0ap2Gc(i}k5L^H;dq9QyBgJ`rACXxqJ3J`~rY$5UES z<9Wu5lxZjHOLM*6Uh5yU!ca-gc-5|by)RsQcxMDlAsKH(U(6?#X2TK$BBQ` zKcUQlK!SSId+ISmy=*ugHM7sq(mKrD8M5yIS^VnG#%(eaw00NF5fJ z67HAzU7#D$nf_zo!2MM|vh|5IXF5tTq+{BR{_9c1DP>%C{dO_6<})`NWsW0f*0QRu z8+*$$VNr(FjQhzu4X*!Qv{p>Kxp~ThC--U&s%L!CJUWyL*sXXk0Kn^t0&Dm3s{f_WrN{?X|X zgFHl1V%stu(q@;^-EIEs(v^qq=ucI@y)8u3A|k@77JLt`5)oHOo=h9qmUFqy+#9Y^ zQ4OCx)7Y^>Lgpov;tW@`5XO&Q5EUdhWtZVb5zf&2YA|c4Bwu6L?(N19f?uQGk?}7T zm7wjd&zgQ${rIc*UN1kh{l~QnO<j6NY5oQ)XzKX9*zquM6+Io%vDev!)7HIYpAb zrFUzo)9Y89P*>Dp{~_XV<7s0k{?_49O3PBp!gl1n`0O8`F${8`{`g91yZu7R7e8od z`;Sa=Iq;{50VsHMBD@8kxq)~bMZX59-GHSl(h(VsJ~5u>y6w)_J5n_n;u%0Qvw^vD zADC5EPa=$O(D!QKyijuZs!2&iQ%k^;Z_9 zsAVTV?xL*4gIX-p^uBd@;IpvapWrF&yF<56cr&Q?miq05(Dm7hy#-y7cU}*t!JCQI z3;dA!YpwIQz(NHG*@TA8q8 zaF~R&SD$$qky9ZI34JdwmhPC&tQNkG%tnPfLyPX*&WEy%a>_Axw6dPmc_jF(pSM{H)EvgF1L5+VnW-WM!3A-JuN}B~O>;F#e|Rj+{9V* zk$>1f))XxB0ITiC-a8+5v)Ema3Zrh)=ic1VmW=0*nzhg{#Pcu05wd+bth@O+7HC}p zo2h+yV6o!+Hq=&RV>Q#Im@BJ@d|GM$w@=y>Gp6%xNC6%eTWf2U6j?j=oVnJsJp@mS zFA8_oJI^0OntX1tfqF>yMWI}~F%~y#Wm{t1`y1>s7oukaDQ~Nktkme9W?@Iw5 zT5DViGe{2pxu6E1U2aQP=B`fArqmgJ9s+|VH1r9xGf1(&U0Squ@r75mYOPcC5jr)f z*R)81Sow_{n@a%g)!U^MI6e}NXyGG?YDNfV(fR5v`i8nHCo@0$^36VNxg^a(u*Gyw z=IK}8D!f2(mqzT1ywB{8Xu601+`ZyG>HfADhW9(pS=`6qWi)-a`s@u?;A!04YNqCQ zrW83=*iX6s638`13b`Ceb>6=4E(G_%Mr#HVI|9%=>qs{qQRqAPRUsTEDQ4~+NXyaoG z^&|f)@6&tG)NyF`SDv&XjFR%!2TasqXa-tV>+}(N)PeHW8Vwi2?Cg3_u}rnHNG6SY z#hW_VlqHF!8#r|B74#_8tf8WrCSIR3i;e-5O7ZygYeFZjY)}E1q(_J~E7W_z#0dP8e zpT|yMeNXfksPObdzrHx9_eR_xr;GSvltINai{Yxh3g1<0satiNw$_>PgUDOGi6(1S zY@q(KxPqnol%@fj>r`F$jC)Kqn0A@1?_+QD^h{z7M1`vM5_i@QhX4ESr!;oc5zhxP zRzKQ(C^-a0L>-ab9eOZ-@Copzj{#cO(87ioX2cJjl84=ht7nugFX<~$jCU=*0!y7e zONj!<20864vKHo&yP86NC+`37adMKnTkXVlv+e*}S87aWu`T->nR4DbJlWRs+~g$Q z8KU_79g{YoS#a^Ps_X}6xJw;NSAzZfrnp;vUiYybqZfV~=agl8qSE9U4g|5ye6kjg z3Sa+C`&;QY0lCAk=km+Ri+)VK&C1I(vyZqXD|$D&f57DsoRA!9*8OxuD3^%n4@6XA zX)ff2NXbpay!cY_d3UPzKxq8x?zKmgO_g@?mg>j$%1jN?7s^?HxlmH)kS1^@qKzEt zwMyF9<%vE(-8Dj&<8($e2pp!8$7R}OEheF{BE}-k#%rs>!}%flZ50UH8i`|$=!+B% z^2ObxS9dORS0&|+lhviP)*I>@q%n`CbgxOqLfX3dWV0K}qWK&{Ju&?ZCD-4t%LwJt zrjk;EYmaCCeZi1EkBLmktBMQ~;VrN6>8}YYi(y7p-kvGtAwdxQrQzHEB`;X+8p2@! z@N=O6S4K23!01Ilfu3$~F1=6FrRSca=|`?DwqC`+D(CY+YUI137mq=%}fo#Y>qf7E9aIhS3*)<<#x|(!cxp^8t+? z;*hBX?JN5vH^t>azVW#G6_;!4r>7Pi5a(k|+g|oukj4K_C~_BVvA<9n70+q=rM4N% zO|I$m^&+}VqA#R3nhTg0? zgD}WZSFSrw8R__4yMfs9M_mv6nADgPGh({1fO^lFJ@1={o9Dkr*8C9_m=l2vVtSVk zvFGEmIo6IvO75XOIZ}o{`VQ6?2vriBMQmgT2L`$xLX#ZtM=yOp@;BmHq#V^u-rc!9 z&SLl%<~Gi_p%i!`R}s|QE5%^^xvZzc^!$Fd@M+(IVI;K{GjV{&vSXW`gWrX(b*Iun zmJEI{bO0^Z%9`^~?IfRVT@UaWgKioH_~u`(mo+T2z8&9A+!oa+_*nRRimzQAc3ETM zxHNx3lXm2P_|upssu3bALg&YRrCK`axEzOTq)A@dTIXOC0A@GIrnmjOGzet*m1cSs zNT1H&6L`=o9fJ=0&b(ie3cpzw@oc;_!&cNyh497al8o;{PEMY8n4QmH)CJICM~|M(-~VJZOD8#^ zg4nj*7j-ueL4vj`@#({%DUq)8Q-h(Z$RCh=liHOMAfvwCs^RE~Gg1r!N{Aew9a(UU zwtVSE(HcCjX)pP$&MJVqPJ5)~4m5av^zpz=m1N1%DFLE(srq-b*a-!>Xyd-1z@SHV zM^c_xQU@;$I2w~ZLj@!#`*8H@9K?}KnY9gixaQe@{RC-f=*>~njN3c?=L648ioX?} zokQf{;r3^TeJqTQExQw6^A0rPEqM``YWV|=z;aw3$lhFCzyG6(%uu_WJ_Voo#nGhXneEE8a&4hIc=DJgjP}c^Cwb9+0j82bZx}^gQSUXsE9II8v z^^L3`@|&fk=KPzq)IM}gjnIvjBvFGnh$`!7fZwUI)Q{`?n4#}7om~F z4XN)OWiCJ=`U&7U`XFL>DCmL_VNeJ}n3#8HdXrn`7Ur20|EoEKMJWMoZbq+d0tXP& zN`NC-Gg4;@U)?axs z2&zF$!#C^}p5}q1rZ^>R?%cs_1OrXO@Gde_vMmA!8`81Vx_rb#X4HC z^tMSF$ZRLlD7x?=^7nP42S~fI&(i-WM~BKiKsTp1L{A9_+)jT@yKROCG`+~)ua@8z z2fn+T*&S}X$Rb>nz#C&CfNNNA`T;MddF+#>H>noV7aclhe-+$^@mK->Z)j?+&;uYAR}h zchzpc$@#UDG79D%+L?=;vi21}^dZL{CgxxR%r5sQ6UpaOHX~*p2df&dM8PQU!SJIO zv9{yhP{Iv$r@#OEsb_d+iJjiXMS_qNTh*O~VXr><4qR=AZjO2cjvcAhpfuUMURzoP`0C+6=IicjZ6_^KsG6pcF#-k}R< ztA^U!L?UMy?OTI2q_0Z<@PTS=i-T~bqj8gdd_-{FGl*^@c!UUSr$am)kz%lfUr{%q z|0}t@{+MxAd>PJTF>>lC$IlrtK>-Tw_FOSQU-z5yh|=;Wh6K^?J!MDugD*h9S0)q- zh+IL0>q!8mikngHOHs$Dbbp7QAIp+2ngV0!bXN>+zm#MwilNz~=Q9O?c=(@0+5vH% ztxfh#$n$QJOmGv7^3d|ky?Z!OyRWxIL`{iR&fk%xD0mMEVdpG5X#`8jRBz$crH_11 zmj|L2r)t{IDQP|1+Fjp3O`A$Gh7luq?n2gA#57I6{8BD19r8tm@4pjp4|U|Lq-Rc? zR?7I*-r!ww1gs^Xd+Iq)yfSbYK&T9F`YKP70WWtNu#Tn)n9%SU3X5rVB%9EEvB`I( ziZF>~g=D&U;szq>xb!V0cif*vT#W6~P~ZI=`mCIU;N>4)6s@E$_j8!@DVooZf5p%S zcGd{|Iq?TGhUWK%Cd=fBQ&W0E9Wj8+7=3~k z438ThUuh@V&(Qbl>V{__?`$%>z;l96cS-Kh+GXnPqbykueS!sOF6*t zOt&M9$N!&Gc{$j2cU zXXl9v)SymH#BrAy4Ide5PD0hT`2L{ih$K*4yaqH+QTh)7^lkrHL{VLzggYr{ z)co3j@7vk-;svs%%p8EAA`r^V9jJn1+LcyYtEzGQ0RK!bz=pn%Yne33!?MtQH$I#v9sV3k-2`d`X!10(ec(3(@M%vr;4oQ$T0E(iPhHPvWT9paUgdg@ z>G(>@C580ItsWT70wr}U<4r$*26{27TvJU9r$t$E82WO(TLR7eH@l!F;}Nb%G34ak zg^=hDTb@P7J_gwT@L!K2%}HeTyb{o$z6YdmdD8l&_c+DRCE56Dx7aBP+GDXoRqbeQ z3i-0n#3p`bpRZ~t)FKg4N=li|xBy7Bsi6o4e`$A~892>g=R<{oG1rbkaXmSLt@0UnL_B8KFKf$~a=eZWBAD%!|Fuue z&Cj9d$MXW%EwEeNF9Ch>c-JM zhGPuFP2OXWVOO2oAN(L2nK3jFkIJd32`SJYD6M&EI&;)U{Xu>+Fb7>hiB}e{q(gv# zEXcPOhSqxjAkUH1#n_!I-$WGTSJ#J@G{oOn<&hH5^Eb0rYc^Sv}r zqUXr!lmp%TLs=2z6l&qeMtQcHlBxh|qCFGx^9yoURO4^ihp();>`3s(`D-0`ZW(Z%9%BgpBoE!RqZ6F; z_~&w2%<`n^<`iv4?{xipuNJ5yAmsi4LZIkKvnmfE7#c~AScV4x#z*K-{EIrj&{JxV z*4mA7>!!=!U;^%U9)G4C&5|E(Y43deSmcDseowE%v$*u7=Nj8NRp#1_tkl$6X=GZv zIcz=vmiOwov4lrLR29OnrH8!L3`u{7uJELrrrBZOLb&0Eh-pfV=2O)Z}8hn_}+c*Tvhw&oNh{UAWdugh93jCy2PO-|8T^y#_PJV>5;> z=rX6q47o?+Z9)uDztDrqFYZMCea8r#~&GArt{k{6dop(rw0bPB@QsN7InLw%#DUhpP9H@Xr-2FJgoZ zXpUn@O73(;kH0?AgM?>aZ>axuxDdqw$;znv0HXS#H~y1n48J3vdv@FwU~rz9Vg{jO zS31PsI$q+~cR495)Ustrhx1$j2z$W%wbV9ADJ7NZU7zyGJGy2XRcA~FBGZB0U$-9$ zKmx{-^@;&wDlp?gL(o>Ip5RI3rK9K+M<;)UyEGebh3_|$T5yq;e~sYCP9>;V-c{rS zD>HrM0M5|I_l=pqw7OhRN3}Lh)>eft0D2(M7Krm3rw7gXo|mBn)Jd>Xpa`AZI!pRa zoT5aKRGjH@|1&o)+IiA2+uA-QM;0QpGMI?0aZUQ%kPKpUnZyP1 z-fF2O9vB-!ckP1PrgG0islXLGm+4lzjV@xCjZ&BRLud7f{}vA{Fhp^ZZHgh)?ag*Y zcyby0B4IYYE;Z~_4dkGfK#1K5q?SG*PsTpk{eDzHwpjfX#UbIN1d&TlhqRrq@1MHL zUKG0V5(muHlfLrE!FXT z7cdJ8m@X|vf$7TqAvxs_+M3oD+%$?BFkdlupVkKUjfe^-EZsXc(ji&xPB0ldHS@c9 z`*Zrr^{L~oyXEeBMO$}XevG2Xre5s;Edh6#VN}p^6_Y5|F`hseWt?I#jD{sK{YpuC z1H^-F2a*4OF0BjCMxspAtvdfuKd4j6V}YG?K+R``8H{DJCxlJ|&K(9@e47~DRwSRS z`NKr&du>3}dKT#1=@_5MQAcpix7x+GO&6A*RV$|T@8}Tb82UK%=sT{}~^cxDK+1FR{PoDb%RM#=KM&SCuqt_nOeF%n)64oap-PLMP#Tn362l~tf za#=fGvx9--oUr*mT1e0sz}K%y2a)C=;Z&;6|+NZgwKo zn_)EScSpC=)B-tee+onXCuQN5pY!Li2OYr?FU_-v!^HMAHq-*F3N+`(r^%Djg;R@th#V@}apPXA z%GYWCMmZ<{xiFQkRn+FW&qHQ@_svjs5w6ZC^BKr-SjpwpWpRT}D=hnyqtQ3v97uDS zPKY?Dz8mX7%D=VmD9sdP(gmW(DQxtC{}Y_$#@nk{9lwjG%`q*nH>=acF|-T=#RW{G zFw+!;1V-g=FA+qz!Eu~AYd#KPD={TsM#JJY-vDF;4Lx#b!3=SS7U|m%X5_zqrR0P( z@J+sIi~5ty%)T<_NWlxx-sIf>6L9nEF5g`-ib-#7|*iKN2h{TA-V2!$VX=0 z5K<#=NzAY02G&d?SWg^Y$yVy=y7|}*&VQU;+(W4EnlQ&*AjSLp;rFxT3=vw8kTDH? zHvJ3Gv>{pyh>xd=5DSVXkE`-6fP;P15(1dgTsH4UCsgI9dum&mlpyAZsI{~C$8AyH zPwrlj5Zffl-t-UwLO6aLM9W<|oy)Ez5A6%rho-Ek5JDi~dPsZaPg$dkm&-48jMCJS z11>CS3LJ6ghW)F*5I|X=7#0$KHB-|!-asUH->;1g#J0~MwUK3o#%GWWMg^r1M3ayI zzGp_Mg(qp2Tm4^s>y*adT3}J4<{EBcFaVs@r6z8$CxfBhl>xlqAZYXC88Ig+Pn`Xe z9~m!frN?BrBr#c@e(S5Jy%p%|tk9+4KfkAy&$cT$Hv|QU)vnuwGb8gVKpRSU_>@7$ zmrHQ5%lGabhwrn5Cz&K-He$u7=kuZ3h>v?L#{x_h6FbER^(jlDY6;2CF&kKg!UdZU z9{Izh{Jf!}7;3$9Uav%DqnzmARas?M-f1_`A=?o=TB3KkR1{N%v0e#?ZK&? zG_W_Jwa$B=;8MYwOMvT8eJ$DO{^?xh-RLQDLzw(u#W@PDxdtRd4pR}nR=~3pt4&L;P*eG1I zc=^oZ+7!VXrDJ4)QB`GVr8+Kn8eV^M*iYY0(A+kpV4fl2=O+OMGh(o^0rz>3A zPoL)$piSW60U4GGA`o{ow++hP=%dEKRln((xodCA(&*v3cs;AidPd|}RZRb+UDM!) zdJE7|mp@0!#t_TTga!_^y86>fp^*_S%>$eA*xeYOf5qBNk5`qe-x1Dh4+dj_ed4qQ z_zm62p!#x#C^ft$N7<7q1Picp_)Btdyr8f4hK}Tg9y}YY5@j0Mqy_saRlQvqS)h?*vO3Al#&? z-&6vpfYHfU6s)axR^*d)3Gkt<)DetiDa)aIN%_?pVVY@ZcFFM=K27M4nPSL=D2Z+s zf3%Ng=>p0shu-EoT}Pk5>(~>9Kc*yo{U6)|{gY05$YfMY6g1wm zoS5$#qAxk@c*I(-D=Q0Osm_+> zMwZ&`J(}M$OaJ1vhx*%{obaTNT=sV^+G?q9n%oG%#O(3MHuTs?$Y?6B)oeos1d-${ zMsh@4*;XzFHyvu2--@**C`3kB-QCm~zAeKzgM z>Z`W~Tl+WoDs0>N8}jTt{f#c1V1Xzfx`B_>N4mGJpKiwqa zOv5&{Uk54Bznxrh&Kgmd5rUrdq)mttqoinv)kwhgvj<7pB*SgbRMC9IDM4T4UZ+!3 z5h%mIV5L#PdXWmRkr@@uKG3)5t*!M8eQ2keeEAf!=SHa?6Sd;Ow4wW&^VF0{$*qPd zhmepPQ3C@5gLevh%e2#6T?#sDvog}tx73(cP7Pxx_^ci-$m$*q)i4qd7@IXpa-4c1 znBLEMdwE$mlbZuvgM}K@)O21f>>9^3>O?PC2nd!WrHqLsv8>#VY+AC38xi%UY2?ST zjXZbK=Tm*KQWSFJcbP&XzbaxJu3vy#Nm_vPg-wh-^&X9c<_HB-2*ha(!vB>uL5JQY z99NHv1lU^e<;Bt+-8?O_f%01DlsR9Uh#cvkC;lN~$1QCj>fr(Eub{vo*q-EMqYe1Z zHtj1dYYI>wvM*a6j5zFg`S{%Zrm1ac1|Sx?`%0%quINR2m!Jp7XgyX8PjgN^vbtRN z)$@@ZT43>(mTbEAk*-1+!TP>C?RQhnQfvI`YL}d^#D*WsuT*2|S38A~?4t2a=Xd?* z6V!y|mww#Dz?e!iIa*wPdzQ`y3edP)Oe`W@hv401G1;ojE0B%n3^%D^cFsY(#g`pR z;R@(Nu3UUl+nH^4!M7~I$InX#AxZkLG(L(9^(NHZ$ysPwgA8LA{6(1R*iA_gRqu3R zND_thEEu%wte^_*kuC0|Rtrh`oq)x!1LqC)wjndrs@^+Lw(9@nLW%oOw@qL(5r_u+ zde?EZ_GkFr-z@X4{LzA&>hy}f-i5rINfxN@{-vKIToMuaCcc)nP4A-iYerHBw0FCk z3gPojFmxzQ?Qc-#yU2Ed89#;%QiH$AM-!Kop*L(UJMikFJ!c_{3fn06HNLBv(Ru!C z(fd`~wlff($pRmbYQM~fhf^Ctdec)@xtkXf%fua$UPWQF?u zsD1PmEmA|jQg$D%8NfD-HwurpN4K7nm0=-Uv&S*ZZ${fEC4OFz^)P9y(L(oTC}XRf zoeAa*-?x6nsCp!KcCoE<-hdpVm`RKfl?m`#JI}`cd4b$zENtN3b?U*{)ilVFbz%w+ zc!FY7?WZR8Lqk6HVLz01d%SFP!uZ}!I!0VIZF4>!6C_j@BKYhPfnaTEWfgm3bCWLj zoxds25a^NrP+EqUSiBNnSQ1itCfq_joEN{pU=UfuzLn-KR&&(p8Y;XAmx7D9fVE806Jao1)2I9_m>GRE>VWwRBPtIW-QC*IzfjR4K+|hv) z^#gmK^fNG$Hi29stu>fdSJz@$36+!? zuF3=0i0UhDjUH{=@QkrF+;PY=h+A1kr^~?L&le`@^;|hdJ={z!qP^2ylz1rgb>_ zfe`4ed!fXt7^M9Y+XsMYFLj%}(@>^tKC+k%uxy(7YmaCDfE_SH)lafUK{%{Hcne zDV!l#=p5%tM_)8}NZT(8bLeZY&pg#fH#~HS3ntKFgGo}!jro1Le?FM9Qe5{m%ryU- z%}n=n)&%-2ad~D}hdt|DLxH z+jY}rME}*JSt8nR1YOV!j31+t^yJI3dte0g&t4u?BO`=Kr8jd+>wfI$&2&`@8p*RXA6;04-;MCjGnqDRzzU1UH+s z!H}?vI*1TB=Fa*^n2C!#xbsZfG%vI(6gVoapyR2^wll)b4Kn&$KXX!Vjj{jBP{g}9 zQ^U8`+a#LBQ%nz&C$37r%b^F3jM;8ckCNN`wyJOVt(za)5{+#->?f`o&2Vlyd>(34 z+F@pn{NVbp(MQ{;FjWr(?)R| zkWwXlp!hKdgMgLxHTz8<)SK3KNZ#J|X@YJUJq`65F`*_+B0wJjC}|Arbgeh!@hncd zDt{+nEgq!4?`QrK z5*=FEJ8qG1s%o;bm3M#$Lk#Gs`Q3e)c2z%dSJZ^W*$#;4CfV2Y?((QY%2so%fuGP^ zK@W9t?Zo|L1o`xeLBs&UZ3a)<$N7u znY{ds`KOM`$&Ib=FJG069>())vQ3`UwLTHx*nJRD^@9;<@PjyFZNT;;8JvHVWW8yF zftRX5Zc@?bsG?wsdCen3;j6w;@oagJ(QcfN_%fOn?nEMj?Y~&V{5K~hANe>lewY5( zBf()PoLOV0NedZZzZ4x;!Vc4-^OJvVtlskDk8$6j%AGg<;AInQ3+$L|ZJ?n~#M8qe zuct+5qU$5uwM~F9mGf4Ux?^{^7mZnJ&L@|$JA8Wvd3;HSfG}M~X9pwz(g7_9H2pbY zx!QQACs=h8Wja-#0!vo^1x17g)y-Zkh6S*_XAR{`9zY6V{jx#D3z)?*Dbxm9&)x{wV%HY_D&%zFt z^#!A|rhBTZ`ba%+bbzxzt$FUAa!sfcHX%UlUqvw9o8qcdp^o>rzjWHdNlb!`~_lU3e;z%`=kr99Dmf!Cp6IjU$#L-@o zGxNpT=o#V1XnLfa((gMl_452z&t00R9rH2K%w=j)zERzfgAt4t|BBJ^-8kt?>R% zb3;6)^88!a#`eLRrM>puS<$!EJ8kK}X23MT9N}!`4tZ#47uFxMQZrd^phQ~;C+Qv1 z29=jEzU{Tm0Op$f&EDQ|PxCgGP}GBxDbdFpqCJPceYN+Ew%f|+_>{_S3Ri5x^_({a z+wbgaxJx-TL=d3&-Jk?gv6wG`e>Nz>mUZ=Nwco?^9X>CF>$mNnQ}Zv*Qk zV#flI<21sNIn!%<9@TrMgHQW!T-gXZd+Cab`7x+de+lrARA;MQz6+S*tjnRk7%Dy~ zn(pprHwj5q?PyF`w4D`{>!PfB1NUB87wNf>+dWx9WCh8@CVT z*l_SUGo&{zD8mt4CIJ5T(X27(K@tRhzKg$ns*3)0ekMwA{}8U}u!S;MJEc-mWZUFA z!zt~#fjx2HxzKOAtgwTX+(R6Vc5SdE@@Q?6HeD*c249dz!z;N@IWu^r!yLvLQp=22 zPU~4Pz3YRn-26-E>kj-Nie7P&9lPMJ!8^Zt`(?79&xT!^a#c?T=*?v7iqpXB;UPgc zAS!c=FJ4VPv!}boVz7F1`SlPs7f3|l{@ug*Jenr$NlzXP{zg`sZEy-TDx1&uJO#f_^hndzx7BB zA|}&Ooac#`$MXOW@C4tZ0lbnU=o@WbAKWf6=|9 z@4ggbt2C;nLm%%xUhejo7!u00{^Yg^ja0ZvDlw%RhWu!bqTHiD+{*_mym$bIssfE` z2<`#TSchL?_xgGc+aj}s%T4#x9zxhOWM$UdT>vv;_t^1xh*6%x+2y z&qC__Pq2iS`Uj#Bo+Pcmkeeu1Xrn*aNf`B%`ve^bK9>%@QpJ_b=iapttYs`H7At^S zOCmTOFU*PDAlUxf94uA8AJHrT$0Pc|6;(gKD;muKYr+p{4#df{klJcP7kM!7^5hR~Z;KU>I*gmQ3JwA!{u^~6I(;bnsOWmZBSN}y)mFMQ z-%*Rx6s3y@c#w16zM_*8*IW>aler*%Bg@B&Oq!m}n`;YN=*B-qc;mdEaooQ$aH2y> z&B!XBYW5&Cab0EoEXDn;he=N6fxveI`o9SeP@x8b;EAYms5 zhd7;n8W>x+Eo->1*`4#YjT`xj1F3IW)4s9E3Sj0Iynl0o5d2zUhxJxk_NSH5HhHWQ zJm%H_x-JZBrirf*2zCrohkt5DLh<3Fx^@5{k;Hz7Rr@DMOWk8SD7CXYsR)C8OO`P#`9G4b!Xb*Ui_R|H zi*%PFARtKRF5Lo(gn*P1(o&MUfP|z10s_(!($c-sNQ)AZg3{f&^DVz`{(+sh^WM4l zoO|wlY3o{1`2or>Cxh+ZwWnORIH`})0=T^7-9*4?X5?HmnqwIUrLK>iGejAtEJT{6K~j)+ z7!6F_Jw_I_wcojb_W8NzpWNze-Uff2Nkt5piHGQ&h>%9V54a$tEm@9Ofd=&cTN zN_rA@0#rU-v(47|v6uw@zBZxYT7N%Kh)YijD6nG)HamyP1Vm09!h1Nv&)sK}iFse{ zSLEbaxokf-1kQ)j2d;yH@q=IgC>uC!}pM+mZe3H;mD$cEl2|Av>_ z`2KLdSny3i@>|cnZ?EYKgo%uCbAcXV6(44Ra+pHzA4f*-gr9yAtH*tEhZ535f~qiqMlOZ`y*RRLy+hxZ3vzOD+?5niMcG87lzNt;uUTI8-mRg_ z-2-DMw0<+yt9X3hO<{=v*y%pZil&ro&&1PkK48n;(}LY2gD+H2^Pv^p#Y4 z`zSF*^|i61&#qf9TQZV@ZPun1#8{67k|n&;e6 zz56I8GvTYh?$25M!7~2)X}$5R@8%XB;{%n3M#-82S1t3wO#YFBI%WH>o&p^QJk|0U zA*LNa5`2&LAi+v%W|DbIS4tEz5-Lg`zgGa_B=p# z&OS||g(oKE^BG7r2ja}li^R1(nfP-m0ulqW+1B@?zHJJ1&mb$5j( zbm)e@-XR^q1`G|7k0vH}cb|KpMU9eylAl$o;2#EjF zYDfo0VbRfkmX8);@Idf)qS-6FYS0oG-KM;5(K&^WZ971mPJm#S?Ek~d2sK7VR~auE z=_#V+FF=gBtT~tO5-9WC_o#(JlX%HBU@=M?{ghg2Z80-pkHr`;EXB;;e_n_x zbD)#)yrUk%u*=;wXRiYb4D|L+d$MhHSg(jc7+?8QD%-uE;+Ipc59R2<5B`iN(gkxL z3|FVGR0xw(zGX!qsdxbc;+lTSN!*5hI#Sh!4iQ>E6jc_<6|{cOLB9gGsc;PDWFy6I zc!rjK<>EcF^^TCcjj2@=Q1UP$Ba1b~1K!lP?bRnp#6??*mKM%DI&1%YoOv?Ynk%-r z`uFSq5ft|e7_^-K!Ls~|@zuC(i)F?Jm_`UFp1JBrfw1mc8m6`$6fNeOdbgqCl43*T z4HySNJ%4Zg%QSq$OQ>f-EaRe&4*tSI+!B03f#+{=c3)7?&CVyldN8Sxz?g7x?!bdP z1EesATvf5&QH%Wb>keO>&k%nsZcI!Jp;-PVZm$0jsawaH@~vMdGVdY4TYJ@-*w5B% ztK?B&;1672R9_03Z5k6E-m{Gh>2MyEh=Xw@nC4!#KlI;9n}V|bU~Ogy0W2Y}$xzmU zB@re{5P&RJ>%d!~y@Q*$1U`5@J_0)r-CN+gV3g~ymD@BXRA1Alq9d5+?X zI^QZ1B)e34cgog($fdyfW7hRg8MWCD^cigFeA$wJ@W9Eoc(L8z=fmVJ2qdu!A9bC+ zp4TALB(1#XQu4)rKk(A#DQ>LZQKvX1r*C57GA=RfP{c)QOr4d9$adRtaL*Ha$C{d) zR0JVuo(XK{V!7Gv)hk8oROgBA(VM4;nS>Ch@gMUmoqnlrxc1}Dq2+fg31pAGQFNIK z=&E$X?MOqkaX zN}v-#oKf`nrTex*hE+nk=-wGDik@Bw6#Z&2HmU&zYLr%hQR$iym&a z)8p9qS-D=jKRv>JQr*{I#*!QbFqfLX>}*{D*}3fl?1%_+ieW_dkpULqgf{pxqOJPH z{2$Fqd_G$!oN-G##JZuSNCL^z~u+0Ad4Kh>v>7AYzvyw zQNJE*fZQ=N+1~{K$A`Kz%GdD4oozwh%uqbE$jBpQf3d5kD8LUN#RfsWV_W;pvu8R) z3jrS6dc80KQ2w|8e|3VK!F&nsp3A-K1p{Ng5Xh|*W-1|m+M-MFZ@-eqD?jV-K55sb z?uPS9%=?kT@$tv_&s$L0laxGs&dU?vqP^MMCto332@N_fT%jOJ4Fei@y$!%Ne$5Kvq@nCGCXXw8YW@SZ#%T8d6aXz`OZ;E#1S=s z+Ff5IxHIi~iw9G(y`b~gJzW!@h|XaU$PRI~bB*@`_UL`aV(kERyV>82nG88kvNole ztRSLY7ZgyG&+a>X^e38vk`y2CbN09hch+=Os&ykbbeL@I9$@_KWc)T74*_XfQR})b zb=yiqNc#b;hHaw}g&K;QV}RI)`b_Stkdq`Dnw)A0ECw$63qO!ffUCMEF)55~KgOmF zv@=jz4^1wzyMHrbMf17W2V`>{G?rQAB%iXaKJ=v`9@*NI<>UaUaQ)xyRGQe%ZOtd| zFLWyysK&5}0MDD0eGo^a7gOgnp<|*6wpTsG8WMm_rx5_y+Ga@&eA*D-o3Urw2fSk~ zF0;X3ib8HG1NL6NwoFI~{tY2~A-IH=qoC21kIH5?C3Vpgr?)d&&`9kMlI(md{cnkE zjn9@F>+5w%QDhS&i|4-5DSFxzwNF3LCQS~%Bl0kb;q4;BT)JbLt8Q5X8Q58y*S)G9 zhBtt{NxWE=uR>JkI_OE$$n)w^Q@ciql5dY`(+Mk4_N>&s$zPqN*j4qE31V>4k_3kJ z`WghsI`V&)QS(p*GJXbVAK&(**1;>vb7#d3fv& z(zCdF8vsn9Ylj-t8a^%M%QWJWDV(;Du2+h|`cTByn*1e5|g2ThQ|Iv!b31?{Tx(_0_`}v ztiOmGkGuZKb?+aK98;M%t>+;DhpnyQG=c=A7QX;E&pwW_Yn4Oq*K|m z95HHObX4*_w?o-j&|9(-cr4Ymsr2%3tfirr$TZVEh^5Iab~J;1^&VR^u%n2(hhkeE zZYZj|jI(9I8*bo=&HdTXE?iLVshHh8HN|tE=anNUuT?=KFyx8veoVDVXmB=?lCfOJ+`Y(X*X*%Q5+Ji2ZZV&EKN z|41%hjGG-zGzY4a*Lq9z9S2*#V`|GzMO17H5MCIjdFQw6e z8PPw!<_^jD057nD3tP4jKh+`oEwo(PxfuukJ(R;tJF}uNSSn6&g->V6k zJ}VL@K8$(4@8v5iGO|2UfXt)Wz)+`w<2&gu(lW89l~lsGq?ix}tS_eb3si|5+!EijxQJXK%YY!(7ku8Dd(Taj{^#DFB&;kjr2*AcuUWB@)k)f;R9fYE}(?~WR82BNhmAh_y~ zoTGsFM<^mBJVO}3T@0$2lsVkRP6)YyM3*~%YmptLF!4=~S$q6g7M%^%^*(umBB-a=4h54n1ANk8K6l>>qJuf3CoU9VJ zX?Vcj2S58LalTSQ#+VgCSr^EvGW_4Q6 zo-Krg32QnVE=2w zb2!gZbItxSvHe<&`$q9;WlWy_kfBrETUxOG{DA4n10Z#a6njxyorE$g=l0(3($R8_ zZ&Y}@5#4JoMHyaUO#FaJWE}ArWtvKafMv8nKs(W%z>Jwk9b z9VS}mF6Xha9Q

K&-;shl|b~-xeYPy5T)9(2`XjY{dNHut(G0KnMtse>%(izImSP zU|J?&Ib6zhWQfOB+t>?tROmY}+Mv-5fA=s)(ia?e*$ary?6rNXb3j0;`|bKXiq-X2 z&9rCrPv7mf5b~qfXK8t%Glu}evuztIWC=r`N_AnfRpH8E?bZ2?lnptV+eXMOE#Lz^ zX$%Lgx$+;llvw{M2Pyqw29on6;jE8Qinhlq)4DF(=AFa23>x|c1R(6G{mF@LnU$)} zfEZTb5OmPIE#+WhLnE~%*Y8lTLbWJX=KMWWrlu*(jDZWHA0qRe@P@oB3R%rmzWO{Mp!3}=&PX0Ad!YjK43a%6}Mfcf2@?X~cT z@be%g2&%%m(>TUh8OXs|GzZXKXd8f_A@3nOVpi=k{^b*SHSQ4ry#~d7&~wPh6pd&hoL^i;)K>QDZ3Krt8>~+0#2JLHrdWe56G;2_nPMPJ7mA#IQ!c zmTC;u`K6B^2K=#!Q9orHeJa@-uHp=e@@>%EYuoX5TS*J`8+)40IId=`GWk-xadVIq zXj*vZd#D%ia206tB+Z+}%jV^xr^f?eLfTmhiSjrLCU!&NlFKl&cEaqT{yb&f-*rJBdANXR@XU4^{alO%i^iHB%i0=21yu^mZ;!Ha*U3eYCpCe~NBu zI{Ssuegcsnc{K)^9lHx(J0WmNzBB(NDDlZPaqM@kvJXq}tz5egUURKnxk10hWM{1i z)K%;gjSgpb+LJ*e#}WG8od%^C%1B6X=2^65G!TYI4i6HX`RR(Y6h}>4K_g%{-E^fYJmM(bO=PJ`@+G+o``B&iI z7Hc}=@P?BPeUqDa01!5dQAi}oANX2Y(jb;%S`;*EaV9@_ax(JqRoD}<){V@(Ao(IL z((_k8ymDBm*rVnGV_omwW9YAn*yFrEds^qWe)mZp)kv~Vfhuo%SG>S$7#gD9)Or&x z&Bn-QQ+74AHOiacU?(uU2`{LW$7D3}r%a3rNkg-RjUNx0bsbf{?N<#yq^Q<|T{P@SB;0iU~51>=Q zC-QGgJTHNK*c#g$Qq3`ar(Wxk!`~lRD95sNVfu7-Cs6a9HkUVQyap8%qy~rQnKl41 zL{MpikOI{k5c0x%^>bG|!`nryt*qe9F*LEw)P;bqJn}g^T7>bZq~d?8*pKv@F8}?( zjE=0*M#yHJHlF3j?=t)RZWDYgAH|-!;^QkMzae+Q+6&EiCBheR6F7btf{SziCZrJ} zINa%7Y)1Ttx*}1*&plLl^)!Ve**tZhbDa&9XKrOKOq_*V$M9T?+uYYepssukYZgl= z+>$;WJUS{&yXC?Q#r{Ih{x8pomIezY`pUld>DJ59OUO?X{*PhgNvdh}cr_cO+1Qs* zy*{*c{Mcl0@KaK*-kup}k#|m>MxUDPlVu+BUm};-%?F-S~pWonN^fVu^7Ux#o}$PkYzB+uuO4WvavV>44aaEMxKe?UKP06w_yEavx2gq$%WRzVB2{~EGo8zU&mHYE>rE?k zEewY8>P9w?3vZ8$SCG&3+LZg-Jg`C^P0c(VihEv=YS?xAQ=UTgnCMW!6k`aH-o?;Z znbA$u6sZcJ`di&S4}|tMK+lBH8CLzOp#qOWq5v{u zCO#Z2w^3Sle9rrauZQ`SRZ}wi{UEK)$nw)&-UxZ|@|+1QC!Xjmx%ryrNxF$&b>hV9 zd2bR|+f%hB@Xb!eDC1;uV$X*!712c>-#7X?I_ESU1`Zdg&P$840}U=;V`t48KTLz- zUYMEfIw@zLBAjgmkom{(0yL5QW+h00@H-7>k|JcI7(NUNH3K-H9(%3{|nQrVohj&l5^AG#pu0LI%vO+{+tlpgD zchL%+OrZRXZI2`05j8gscQ~)=Gj!YBhj1h5UBmP?jo|XPD>40Uw33$8B=62 zcL>OO!8hoX&}*gY*5(06aFHDk*kj0feUgdFTYRiE4PWiXghu%0;Di}gEhB5Aj76D*=q~a+yH8VHCPD1W5!{p&F)#D2fWT7 zobAAI0L8~x7mzRz9zl=02cW0##$;;WL^2~ZpbqulN;d0S?PlD=Jwi5t(eBtALl{-f z0a1Lf9)}R|T81X`>{+#UW=x;XOUXOdEpz<#)#T!C51|+M4WNHXwr^u6mQ1bv<0ds(dehzf z+zSVW?k)Y>N0K0-%X91IxP=!NA0S^od87&!MsPcDrKd0yej(E-#xSNZOBAYVdCRrQup_TMTCWWpneaYaDIx0ukkzaGC7`=<|&v#FeJJP};r%44!bc@-1aK=~OO zj@2BddS_H}#3e*Jxqw1tt%@|03Oc+wQt7FFc0}{IxU+ObHUawU9S#1bVEv6LJtotB z0HR(?=ClHZ#Q^|-qC(_Z;{p`x#rZcy?0@VC1O4=&w><1lkTOY}mb@93r>)U{m;S3a z1?Y5xlBssYICCJLAFCk}d{~dnPJxTL)H$5pGT+>-@mxL zYi#b6IJAg|l&##|?PSzz=4vKM|L}@XtFpAj^|~I|DiP^L)nImtS5+;D^rA*RoME|i zDePSaxG9Sh+}`AS|9VAzRiOg`eA2pIl<@&T6^P^&krgdXPPjRjE9r4oDb5Xj?^E*K z!_EAZUpcdnTozbJpk)Yd`>Edmp^!y7<~|d-cxHabiR(ufWcG1Zjpb#8K5F0NX=7U8 zOS6uxiU4o6FcIm0iBwd!ezxBSwRJ_Do5$C`rZOQ=%A&xMWO<^gs!Pl_2%xc{L_sXk zR5K&v&#y>esDzE!G=!+0e}Ikz$a+i%0`cr;lN_`F+oW-7y1rTEhf*UPeAjN(Zx;U; zfGYdvqdvIZV4r#c#FB5@$kTBGdrTXwFRZ6~h>cJ$#-ORWGy1$335VAERd`2@4Jt0> zHvbWbfLb&@ws=Ac4Z}o@f)J=Ic8C|KVR>1I1YESFAes6qxlm`+0jg&tI&yqCTfJs57aaH1?^F0Y9 zq~kF=N4CAAU4%1uqH#pQ4=@`Jj-38Z$`fPiVD69J`b7z`bmOgB?}J7K#$Y!Oy5ssN zgYr6ZkNL;A#!cuffdKQgLgWE~y?1<6ox%4f%NGBtTB~1q$ki}nJ5{XO3O3+DNju1o z2Vl)qYN`5aJH5Gt@&HrjZ;V5LaK99iGNvK>+ZK5AGyh{+=HJU~S*6k;pI@J-5ImlS z5-275e^;M1U+?ov2h&?VKzwX-B1J)BCBM_SO}u=jXWM9u8^;L6&7O)_Te`SVt8UwW z{{eI_-DeVj0@^|%S#KqNKMhiPBuAKWsyNl*-#rVL20OWw_|r_dRg3w7o6}q2LNb+@ z(O`Pe^rZhY-R*io5j^z{eKRk-|LE}dXe?W)f)6sQ;Q8A71tqhZ<~LVj(&a->DwJQu z-eX)?9aPF)h9^BSS+` zVsjXbLNR#=0Z?;R;cdnvBTi2_oo91lWz*T>cR11FcRdln&JOf4jC{+i(%?*twdL}O zaSTjt#d1=o2PU{ML7=nqn#p5lN*UXHaRd5I`J(>#dEQT*Aaw6-=m6y4GB>u-D7D1V zo}L9V+Q9e7p>qGIvcFF?^w1%SwOOm)F-cbN$90v*m9AIlkF$`ag-j`|Y>zVe0i=HV zXv=}1mL4`7k=8=1W-mQsyFMv3tmc*dvof5A4-NtMN3R!h%8z=7bZxQa@r6lx6I|Qe zaQ6jzM7BtgJHAlJ4m@sq#)oYXYv=yBV9}C$xjRHIbswPyL?=vKcRQ zBxfm7NF|`7OS}DU*!C8@_u5#k=Vm<{Gsh-U`>B6_I=bPb=IY$@0CP`Js|b8AK6N9# zHN5->=5DjCsqG1T%Bj^nWg8!0Whgv@TW47IyWw3bD*&-0I|-(y&Nz~Tq$pu)KzpAa z*a9L_A+XP+fD!vlT0(j#C%<>wwp>ouvM}Lz&Y7Ck1_^k6E+rx_o}A|q(e3idn7r1$ z-hiEx=itxRJ>VKsFcOMJ!tJM*&J_?$LK1J7I^%szg0>Plsqj0Zi*( z2+xd!VJ;R|v7{ynRK*i%7HAZGwV_rck)`aS6wZzX*%_KT)c*;byI6V>512wjSEZM< zGj=$#wp!@v9}&cQuZo|D1j(W_cE$Y3UmtnQ%p!06@eSA;u z8GXfBL*t8iN)kXDKnb{+S8jE|WY~G#f7O)bT)(1L*Pf;Z)U|bo@i3X987!7kXZd+t z|H1A|-18Lk?DtCX9EAmP=aDXB#}Uua?|CV4la}DvF7=y=tI@++p2boAoYMYOQVUgf}nA|Je_3oHnVhsTs|Zi(ItggS{S507gnJ6+12*hMFhOKGvGv=CtF$0Y1I*>@#VUW3CzD0FE>CPj`Dwf7QMX5Bgf` zOx~Bab2jDmnZvuG&bxQkwc6qPuM;?hzrAB31+>=Ka&1up%sz-o-e{#US>x(zE zjtLVyxZqk^wZ^H%qMg~-?^2SyVFUV(88I(=TjcI7Ft?i5gC6i~KBZVh;KCBB~%D&j}0GMTZ?4K6MOKW13}pX40DoWSA<=Y2Mj%P(H*u* z7j^+^zS?oHf*xtiDnH@t|1T>^C7Htb=D>L{GYM8keoo&d2jDzOf_*%Qk1NRwgQYhc zkqb&cwS3-)>G&unF8#f`ovvpy;R4yLgJ)imh~d)LjIFpo4wzg6ykYhV^JC0RXL7%Y3vLn9d&M(jR2|5$}&0yKG5EFE7oxY1fz6 z*C(y#|0WlP22xxHxg7w&@=u;h*aJkWdN^it%BPj|Si+n$OYd>V`R;N=nUN>Swj?@AsjPUUK3>L*`?A-jGO@0kp`v%rZU zrZXl!{@^@V3|j(#OK7SBp3+*_LzY(;%F15+4{ z{df;MP4R5C%4;fi%dd*l7Brw(+uKT0hD|a&cM?y#f-BPuH2LlNw~s@7KDQTpuxvrd zcZp%+mw!rRY20|6^wDhr$7x68J?p-mc)Owgjt%T&2!nmeSKBfP{%iv+ptSkw3D3y-7lG^> z47BJz(S2a=dAS?Cy+GAu{yD2xYb99`z>d>S=rO7tzSr25a~9zzW1H!|kiEA4qnN(C z+>!6gs#mM5pIa!2kM26RVd#seK7(+jh8?o8$@6Q5$>T5)G>;SqwF)H9Gid2VNTt%6 zau?gy0J0oj9Lw-sA;pl_m+h}{0Tgi%mboqaT<@ReaUa_N%Dgz}D%n!^!b>3%tK2oU zeF_)~0{E4hd;@@Y*-W)J?5o!8YWORja-3XThUu$a^0|;tTGg>DQ<9Scwf_mW%h64s zzDWX8;{->S+Rk_Uqtu-U06|k|;Gr4oTJkwMHm<4O5`uuisz>r_;!dgXp)G06h1!!e zw<*9n&jO|-&-I^~uqP}wz-v+#N6!E|J3IRo|E3W}m?fd~lF69;y-zgwjhk#6%r&xS zj|m%ivd_TlZmq+NiA@RDA@R2WqrbJ2wzd{lP>{9y7(v7%1pRWvy^#l5?2+ALSPnoF z;tXGhol8|WHED;u9|&NJ7t_SjH3jXt^8aK923#II;GM^K=0d@yeuT?uEESH~eUH5Z zen77D=6soX<7PMH)F_{4@v3{Wkt|~;U%GoeKYU63{lOecwC#(K$x>sZ$xcAX z@9X0AqQkYM%Q%NeZj?n-^m_b1Y~1nCS;Q}8x;6sbqo8a*k<`Ahz)&VdcWSgL)OKzu zJ%meR{bX9Y+U1od03(R>%*5Lj2E0=l7ZKO}i0ndzv&po&)o78#xtX*)+ z7wpaBT;8LXg_L2>mgq^yq$fFu*GG_mq6aRy0(5z*GpD9bs@IVBJ4_>e1X!?=0?7gF-02`CCp67Fb%{;@!4bp7BOp z>egbdO|ULg^Jfnzb}Fh!fTnMkmjne$D%JOCTkW4FMM2YklRz(^th+=mUXihXC=YEf zK7GvkBt|8=N(SJ%+SuBDF<@Z>pcnth#`{`N74RW$+CnpIMgSmVa22`%o+7XMHd8FJ z(si@y8r{T|KiVA!Seq;wzqaHda>AW!d{pl=<~%8j52!|8%}Ij2_SDa9ZPOGu_yNUA zTz)D@%L{Bl68nT&lh}v!q!B>K*Zq#ZOZotR|IEbyO%E$)<;secRbA^PUTwZ!xsD3i`TuH17f=x5Da2% zJ992OCy&MmWSU#A18>z{+CO{8!yY9|3&;9^h9A1cOfs-npcMl7SWQT|`VVmc#JFuwCZ1h?*eP~1EYzZaY?7X|5D#k=1A>#MQq zmAF7KOhKpy#U+de_hk9vkUWGp(!PHBQafKtW|CQe{;NZsCR=A*ydd2kjFU-uMTF6f z`STt3^G-6Qgf6S>=$f4|d=HB?aLQ9P$t7PuqN8F%6CWRl%IpsFy%i|)=izmMe4lFToO=I_6@6G3@rXOF7gVi10P#UypN#ikP18Z}iLzLdPC*x)?6$@R18EM)?@ zhP8J9@2IsDvGW@T@M%uDf?>8hhdshN8DX{;u4~CRr%#yhH_-RZ?#|tI-BSwO%K4m` z@dQqI%BtgyiBf-^Mw5b!s3X!BEX=(^8f5^Cc4D88DqjX%3fd#}BN^7D(%#xd$-V{J z{M(1SVLYG{gpd-!o0+RvW$KEL@*mOjJDv2PE(4^5Xh+vQKOW2%n4*{goXR~J0P?*f z7*9@+Z<%NK&0c7AEr?;coI?-TdPBOS zvH0@|c@?w7_7ry3tPjgOw~+P0Up0$=GyR_EoQ!e~86ypjwqi%4JfL8km%wroOfi!wRe8=Ampi@%f$Ss59Ur&E42-+ubYr}mysZ>rQ#mu4x9Y|D!q9r+`;%TsqLaKhyH zVMrtFn(-Gg8&T!9h_R8Ej9G z$VjIAe^DJ@wd5Dy3*dPkD>jhQ@m_hvR5L?=Q{Wmq$}9N@@Bxr-*ePWzrxR{)J@&<( zP{3+=2rkdxi1m}dxVW|K>bIeA>tT46s1e^S#`x8;#9W>0$5KjFf!)q1G^j^vK4?&(M>bnU8>#nN>b#SD{A zld#z>=8{*#JLHbWPwN{4|FEJc-&RMR@GQnD$+7m^EBNokD2tN zYid*r?McMiMcy(SYU>ekb{@IRA<4Q3@peNwMaOWdH-QysPUBytkY56^T#P_ z7(VQXl3GBXc0put0ZUR~_mW>c7@Gih_YD3BNv{`5UpRVwZ@8zlx{z}`!i)BM^V$P* zkeC}q3$D}|v1`a?nO!=9puD zt&ZwnL08A=B@DmK%JB3(Y3;3xAgk8OgYabNYhS!4ypn~(2S|y)-G+|F?nOX)e}%NU zwW7sU*f?|yA7FfapC^c8rM|h@bslLf(SCK3A%LmGHVj@XW3RcFzx{|UaMrPU%W#_m{<(MSm zD2a)dr4bn;gR<2}Os>3!zE5fcco zaK<5r;gg!qkCL38+LE4sa$7i9eqy4RKe;pi$HaYy^s&(ymsiK>Th6$S2O7-H=551kCo?RQg-1L@qSY=Y-h(gY0!oo0EYAyno7{KE~0Bd(F`7P5@ zZ&6X%{Jjf*<-FiV-J}-Li)*%36~-nHj}c|Q^}1TO8qBJzdp*p?1+Yy>oOu=aEY;3Z^W>n*{#OhfAP4dUz@ zk0ur7{xS4(Z}^%mx85~UdlTD`6YGJCd(4-sPa!rS0-$^!wIR+cckI<3AmwMwK}EyuaLz`+B-n z{LzEX#Yiusz(;*!g?H?b+F6^}D+W3f`Mj}?mitdhZrm*H9+Z1UrHHTP(uj!$)e-pT zEIB~$4rO^Al2L3cAsOfI+Qx{sZu;|~0>hVU0eN{>?uS&5J7qoz`ly>KJfi-Ub!7SA zON&?axwwVwQY+3LJ>z#~Ts_>pN_lHYt4=|D-g5jB@~!<}SmF%FHp+&P%<{Kf@a;80 zCAFME!<{dAO6+7^*LqXShrWj=pU#i#=}NcaeUSi^ei^6^{(>sM9)<~4G2ZEp5{Qwr|54!{-DmZ2&XM}xz%tKw z9Nar$9+WX9<~EX}cswj7k-0xi=LlTh2xKE&6d7L=@SItZsl(ZV$q*&xr5Jq@`~8ZW z?eh{`)^Z(KcR|1kpX2zd!{MS2%{>*HLB>$&3J07LQR;A5Xy+E(cAg_DB91pIXt4KC zR!kzI%u{R0pmdnjpl~3+9N_phQ__aR<0f+Cypx)TQvD_=y^-`+AxF)JrIfG*pXFrx z6N@cDOybn6kSYP80csgV$NKznrrxqS+f$)E*xsnJ5_5Wlsv zgv!r%S|@mKVQ(Wy?qRMH2bN-DNH`L>%+qZq@aZqFe^Uz%LqvU}tJCY0 zn@d!~O(;5X0am@*4<lC8K2I%NGm0Wda)oe9qll-i<0s0AXhk2yVK86AnGXo#-9V zJIcmJ0a$ifMzX4Lu+lEiTMP4_LF%$QcR%7aWWiM0XF5HO>KEm~)0v|M`0XZ8aONr) zZ!hlWwFKNbUG5erQh4|3z7Latovx&HNc+8$yTR^_!9kcV+2Oao_5ZzO=DtyXC1Qi&qN0)96fjKpl6^pwpkBs+uk>(Ini%PPKiwA z&J2F+Mq{~XP%cFVc~XTGW7L^fgSdrw;SHr3ZrCpwE|Y|rC?Uizo1foaWHt;|6`+pu zf0eteoSMW);eZhiEN8o2==$Ur)K8rTndvlb2el0zmKR>T=4D`FU2*b^_4NvxSiVUES<`>ewg=3 zNid?GQ(f8RXo#5hr^ZOCDwT1t-X)|mwzygrTV7t)R#Br1vM-_;OmALVpgXa&LZT74S*|2hAZyF3W^?{J_$MNNKY~e+ z(Zu?h+ol5?@)0@XdyC)X4(G;uNuh`e$jFCaBKhLU(T0!R$SRVo-O65f>aFb*&~>j( z#h-Il?RQ}VQj_aZS3>UfW)hoEyl@c__v6BW`l()^&f4O@ze4x()xsOOBy*!m`vp(S zm(kmfbBvrE6IS&U10$U^Y)TomTbDs&r+=sPXc?N_IP z{r~LV&D>+W=@fd#H;#>)=tgc2b zwWWJMWzlI%F&Qh!{q0M;g zkA*TFu?F81_2b0E8t%cVGy1z^4psyNGrpyb5{Ue4hS}=P4CQ3=r&Z0()`HGa&z=G+ z!bI;`4?TLXWS@~N-olOo#9=Nep7a6=={SyU9#6rmdadF3{!!8XO?Zn@A#D6?_BB9J z4xIuV&>wLpA}cW?=xf)`$QR%ySCf!z znJnzz!ju|^PIcgUJ}OewqYNWQpZT6FN3p*oB_)_oYKEeh$oWoBcjx}RA@AOupNIH3 zzw0`$Ac}fM{XHl-P>;EI^EaRg1yC^=pba%IulypdU zgLFxEhk$^9yM)q>bcY~~QUVgvAuTB(DBZQ>?z8@%H~V^LJ~Q`s=iYO^$LuP6;)7qQ zu6?Ps%BGgmTTJ8)=na(|#B#9*14Uy)IM=?d{_=0Ph>lg+lz){7Qj5LgW8+!gW!^9mVk&qrcc;TB2OXDmvxx5xm zu+E(eFN)3{s@h}MdBs0P(y@Vbf-Tyet3q#7LOeV$bw{-u;QH)IvQKY;_?54{XER6>aq1e zdH#SJYI%{m;x%(ss0e`YiKh1zcN1@phs!|ia2}`IG0mW?cK_-Ohq*zqo0t3 zTtXkESJ8hkfu7l8h2uoVTibmJ)`*6ogA!)E(d`D3=8Mf&za_sG3NrkOq{jM$P``g7 zMHA$mA7Jn+hC&`Uv%cMjuxyrK3cwO*xA4Z^_(Lm>`aMro36OKDxEHI#Vy*c|q4CI2 zuGEe7&}zYS*wSdg&ba3UT2y0}Rc7nNdEaMwx=ZzpfQ~3$L{5_wT#;MYVsd~=%^ZAxkBeFAJ?W?BUe(*1 z6ab#?PNYCwR1b-BGW9X$f8lLNd$J_)IIRB^yhuva^8Wva!1G#v@5g{?6DH;!q3q>5Pn~#D4oro(kp=HT-#Z& zt%#Ahgzryh6QQ~v9 zczI{%%cHGD9I!`5++hF^pu1bTv3_$i5^~evx%SOU^knaPO9cZ}76;w;@#&?-wA<{n zZw{yYC~EniA$p!56M*%;E9xE7sCyyKwKxb+AmqLFe75#Vc%uJBt-;P>hmVCqCIi5P z{gxD7%{7MWY3B}NRLRXU5fs!P#}%uOCV<(T;1?ku%UDDeferZiw?Eg4&4P5?i>TOt z>&BUoa*cmlD^U#LB#lbxz)q$washjAyLQB)*ws{wucWH~&;q3@0Nq*1rluRw?Ed{{ z2mgn51o$ot8$ouS1-aOkVpR{`2Ew6|FL*B9XjU=r5T?JeZ=Pl*Uk5qwAZMl$ng07@ zDg;l}u10EOuuVIE#PP&x8Jfy_cx?>F%Dna{U=sO6$y(W?x^JA3IUaEzu-9q9@9Qfl z$xu`5v*Pq{_U&vnu;A~><*TMg1(R64uq2{-EzXkT335q4VT! zi#t4$$Rr}|;+_g%nP6tQ#D>C15`d6&BH&r73+e{6ARvcj%~I-PaFY%&W|I0et^HZ2 zn17kj+x6lk&GnlE44C?WX&lhuRp$rpGVGzNwznD0Pe~n83J??cI~dE0%xX=_k37D+r49w7?^} zzGehHW07cDQ$MzPSW}wTI+gxM;@Oht=gxq*A*a?XP-v28LyDr4y9lz&@@wX<;CXUu z$|%6Vi1Tw-9dtOt0CdB2+81-ULzyz7w=9IoN1hL=)?DWkFOWnD$Bu6HAD*T)&&1`h5c zkTX%CAqjI3(64;ql_19rCn;XYH6USDABr0-(fV@pbVI5Z5*pEdM>F?x{g>@>hw)BS z4+E`=HxALv<6~cwV50oe+mF)DYkzCu|CADuV%tzVc>I3w7(4;LJCm@Cbfobv#ltJk8f?aR7o?g58jm!g0?nZ~jB&^L^xn8M^2>pvHS z0o;Ljb)gMp{Z|AnZLncod#9Z`5?`_nnjB`sQsrBI%%RhBF9TSi7dF_QG&y8kHL;t; zuDkTj;(z6oUboedhEGZuJSJn7CV$V1BZFRKu9rY#kFyBjjMz@XAGY{xpirDthILU| zl>8*j9-Gxaks5Dr=1tP|+L8YR$1Kh{H8>N4#lPD3(r?!+$uKk~`3fb0?^Mto^(fwFugvHo0q>ltxS!fzKj7J?XhH)-1 zVA=IX3+RzRN0N62A0!TMDK+~-B*{8;x?%T$;%~QpUS9kR^`sps`*QzIV9n^rk^)1h z6kDlp_|mQJ4fY2eG(~qx*Uqbpd%;(UX`;niDjl72&EA}`|8o)-7Gz#|f)P;Fd z^{VzPE>bddIF@EF<89su=bD+B!~U8WrRI8#+p50`H9<+tGZfq{Hb5H5WSK#QNOn(E z*VJVW{}L`EgOp>Qlk?B_B{boK2`HdgP%JZQ+Ge`=l7rk+mJh0)i$mGOm13^t`!{uV z4z#TL?XA@Go%)G4{J1GQ_vl`RJQ~h{XGwX_RMh1qc+ZyHLhEgh~lw%SkdoU5UqIZE*53 zz&>#;Zq)(mNh_AEh(@G0_M|~XWxDl~0)_?ZDWX`Y@Bw1~EUu4isQT{8IJ|JSyTVG9 zEEs%Q_;`~ui=vwMjVYlyWuew5{%kmpxbm|H9buv!2nx7^04}e!E)5Tq3N<0`96$}4 z>dgdl17v#)-%e0lA-6pi7E!DV3@XP-)X)i7Y1L-8iOPSLF(eit*_v(frqsH%{6tJ( z2(3L|bOPP`Mo>~&SO|SxUA;dpL_S;*Js+PGR>97muKAYb!Wn*4}AoZCYD*Syo+xjs_zg4inl}K zGH>5oDp93DmBy~P>iA_Dp|iLtCn))Sc%Xq%a6Y`XY#FhC^%C7NXs|Hy;kDYdG|A(N z(}!R_l&Npqbm*&l1wFRAr=_P!f}yyG0L8t>-Mkc*m)7X!>3H7H9SucnKRBs;;0KQm zRti{T;8ymynK-9kQye^?te!NCcK@|QPeuz5YYWxjj#a}%naLs2=>1!c_Ie`B691Z4 z^7Z^qZg`q5`Wx2}e>YkE;`?_*(4ae99h4A44OwQ--&n5iZMBLS|8@-K zY2{#~dwR@Yv1{PKBiN@s^!(bNgr754higXM{566A(gu=ejX)^>e8d$mhVnp*_T_<27IQPeh| zC<~}NWvFu(^R`)zb$AM3uO#GO*c8K}()Y=^k$iph?%^!@Hkb;u~ zUPf5O3X02JAMt4<{XxF5#{WHuT}5hVaX3jY?ot(S|IpV~TwdtkQMEwQ$gIvU{yk)# ziWu18`UzkkY6F$%8mTwlNvV@`1%>Wi9(+fnF>HCD@BJh9toIOog)PaV{zCRPw4yl@ zzL*XK%UzVGj5A^#2^w2PV9fcy{H<-CP2C;~cfpNqb2bUd{CXdTg%Tz%`sM9+A$USk zF83+yz-!Xwn>9JG2ow%~Cm6l9;8Z@Vx8tRE#VZ$t_x1I$4)|C?=Zs9^@u?F{uVKop z86m~BZbU-XJgJ^Z^*)p+r)Df#%V~^?*bRTP7JYDAqU!+ES0LQ+DPfWx*1C59OK(}n z?ro7z`7Ol2XCzy3;>A#CrYADkL#uBQFYPNv-pl`Pi|z|WDeMQyxG*`MDR|+O8(fq3 zw%}MMeJF=j9)k3y`fR05&rNm_l5B06d-r>OUIt2y}1;t3^7(eM)#4B^(^0R-wxy|r&&Z0XmmN+)C#5yzkLHYx*O zMU`^BDR9BXUfkcFDHV)UFu6V=n#!5Ds`omT??qf6VHn+GJTlZkSHDTk4p_#Y5Dr=+ z5w9U^2?mp_2&qPGPuuV33)iv4Y{$w!BksvLXVp^Ds(#ONRsmSiXyZPpq`0^TKY zqg4ED5L9qwrQ`Bzji3pZ#*7jeT!aU-^pJ*qFbrECg3R)h_;GK2B*U{tI*Rt81Dua+Rg?{ zKJYNo_fY}`7zw2LEK?f%QL$xnelKp?mp$~)1&$4p<)Z%BtNDQ*I$Pf>X9Wi5V_FeX zeLNMd3|4+^UMAYu*`{g>sg|_EX+}t(qH+^6>aacRbint&*6IcT(02xDmZLeG62{XO z=vTCtZ;=^ikh2_~$N7;7f^a_ooo}5WN8IhR_2h>XO4y0WO(S6>p2Ql(5q$k=(aLSY z$9oitdoi{aL>3>POAY3#h!#b6?joryaKumuLXKU8yv?AV_;nuuj^8@xP(ULd3n1nF zg73E@Z!8=2J{J5|>ZL4+->Hhd7B)$d(h$P{Lc*-xnG6l2dp|_B)%#DdHs}2X2DLCq z4MEP94T}w+6_rvM(eoGxl=W zt>l>m@qlEV)U?k`k{Xq74IC=(lI&@AsIypbaC|3@lTvpv<{qtB8*791E6L&zHEvFK zPrIn8{Uy(Q3hdCoRc-W{kU8EJizk^P({gX(ZufSqg4Q5umGGjRt>C^VHIyB$fDlbe z(Dh~r1WDKs{j;(otB3Zg>{CfI2Sa$r`IBfIfN{kxo)(6lgkE%yXoSUdzyKa5RLrq~ z#|RX3#~QvI0GAZ@3~@^dq89r3;5q<#VKj_Qneq#{!t-+eQ6f@#*Q%qxYlB=o+CgQZ zu@$nnR1`!#X!_5HXJ(fm2`>l0m~XeAyQ^C!=P`b|==|4*|NRuxVTN=ig-~dZ3_TjnjNYT zpT1>y&QdoQsI_v;awg_}th9>prA&uBg(9e|j)ilc~k_l^u_C-x6IJY&e4Id$?kr}gc=`E>{!!UN(Y*|l)!jCguhi(! zOj~o^9IYRh@GUsR5+Jx9#)sj zuY&JG0iz*1BrmIY-+yPJA%EHMRN;(ZT4ww~CiFZSfL7GNlkBN>ESk&9Jr}5j(e%*( z+2ZKSGX6ON04jGS5mNTfGFuA_$3#l98so*<-AJyrM z+z&jY2VFUAf!l1A)ao0$F;xp#5~t-D17^tXb?-D!O+7q4&)iO_!2P|>J=msu;PHp! zB@%RrVkbRZz7inz{W2jo*gRXf=#%wB2|1;b2b4Oj%Q*kxleKTNqKVtTvsX7_Q|7^T zMm=@_y#VDW#svaN-C1|R*o3&=R}>qMHeBeW1olLHQ$kx3BXiGM7_3fAObk5sNuvZl zE)=s2`rq5wk+J*aMBn)U3AqsZci`mgBa<~MaEn>r8DWPOw{8HgFCyvFF2)#=LK)z4 zEyL-jWf8?q?#*=hVqvP1{hpO%V{C}trLt%jaaLh9z7B`;=G;O?Rw0Wu1$s*-$9BtQk!0>FN|t6bITmrW zYEj^;_`Qio4PTszJDha9;*AF9k!JP^>eO~;bLb9k%mpS$jU*kFS6B2<+8W;!ZZ@!0>s|?fi)zW+p`?_)AbDzGl&NllngI@W+H1YC0bhCd;YclIB`8Oaq=(Ku&M31^H`p2+>91(` zA69Shih?JFN7uKQ_mw`kOvc`=@EfW*#F6B?quY50i^h+$DmKb8IE5h>1eL$|1ZrFRveW$ncYwMTKnBt5cz*B~I` z&z%C4qKK#sAo72beY~?})Ntg@=*1jflRp-w2_fIjebL`$*J7=U(#B16c^*Y% z-)Pw3Q6D~MfaT0vg32cE`tB6g?&pUcFrI`BsQ$U55jan$I$dIvT?&gUCzsV~P%ivY zEQVqN0=2W2E87dZ=mN5((pINEclC!~lwpu}1rXW#VAa-uU8lnqh1g>k=RB*aubyr) zmLmN8j`TVdC<_WEx?%fb>IOd{Gse3=4*~}185-mzW|oNNQH$)!5g8^dOr|q!F@UTk zDjt>(ug^toqWFsavJ5JMK5YXD;64f_e++)ul{SUfW@Ybvnt{;l6jKPKzmPT*XIo+YOxJLF&J`C^w`Jy+#(J8QzA>H+G%Ge#0j z7-Ay@G^kts!T>28StBXtR+Abqkt zpD)48A08+8i*>@L=p8b|%Jc2Rt?`8w;^N=WBgMtTE!USXtwRXRYaP-sKo680Bz)sO zFZ1d0^Fo_+0(6RXf?}uW)~?=l$q{>(a(?u=BONrr{^I6(HJCW~!CqVlUqC*XUdX=( zozhkKei40K2&sc=e+>0GD+B|TSY)!0<5m58D}O6Qtxp;pXJoqq)KTP)*Zp ziuXm!n>y#Y7P`a9fEu7uz7T%8F-(RKIzc2s9{TGzx*yIvZWWUn@lEaef27TNq*?Ii zjAu2@{B7-Z4pWv|VqgO}n6sBKC{e!=H>~AJ_ryGBC66v%@o!fKliG^X*-9p)6g`tZ zlwb+LcBF$RILb*r$$izFQ)1b5lf>!TtEM~M--)ga%>Qjv9&w^m;*=~!F8g}iC*U|; zPv;O40w*L&o;#258)6ex7&m?rspJKfXHuPP{V02&&GY$9*@ z$+K>x=(L9~AXt!JG?(qU&FAXX#9mOedFD*n=tI{%nj_}+quh~|N!Hd^2&Q%8rVKKe zw(i*nvqy(_N%T$$Z|30zi3~%J+#HMTp-(hqbiV1HBDSn#vj0hbJK5!NdSa4a!VtX$ zJMHkMUipae!iV`XYbe-8xLFGC67=9iX(>_Loc%t=Lr@I9Ip8L!efS(T5)4z--;w@W zj7*lw008Y5tQm;t0<^*(0OkeU<`b6A>dCZIQ=oE$&cCk`kh-?bg{YdTFyk@YPYxUM zJ(K$Bdr*4=EhrBkfcIpwz6Xm;Z=}rwCB?8~5thaH(d8nGADd-|{66;MLtj5etlx%jZ-<_}6?vSHX$?ebbt{8V zvog$Vz|9f#BFOru$@8sT4O5(aCx4u_s)0xpb%3TE@6{!);v_X)&CFDrL91x}`5K8o z%t##n6dEff`F@ijpN~a+*Y)u5@a&{{agB3xN%e*~;u#H^$Sb8YIj!getA~pHKY3Z= zJD8ZcL9)(1E@69!ESOcf>4NR%vS+Kqc6YHLg1VU;a!JQFzVVj!e$6Wf%V!$$Hn&Sb#uFxq=mzv_qar!(CF|!_5kt3xX9#Pm}ip<*A zbpkh{|DY98n9!yCvOlaO0%L;D2D;xKBe5vjw{^$zHQc<7qsYZYau4@2?qt~6dlb}Y zJa2h5f^B*q1|!uP@^x=DV(gxe(YWV_rOpJD)z`-Umjl`=AABu$wTWm(=Ye;FPe0Mo zFbHqilbOEmD%&CUIKV?XvV`+)U-VC-ZWuK(_I-`ZhWOnTUngxM>LK?PLAU#OKY~x% zH`m2ddc@3i(O)BZ?rBH5%Q2G!etVH2ZqHysOK4%` z`=_#Aml%t6-W!S70K!$i^(P(mQY@9d9i|q(k{4XsEyswF)JKg^(r$fuHz~_Qv&Go> z`iqe3n-W2-!im3GZehw{*AQn*qWpVa$YaGVL&+oJVyG~vyDv}l>AK_Mr$;+0AzQp% zn5f}xH{Cc9pJL#P^hxR+`IY#ywdf&fVyJ!ebFdDr9f+N~a%Ce0h}Zk;YXg(e72&GD zxEpf^rGyk+$gno|(OR@Fr?1dPts-oPtW1G_Mz~3e#W#ciY%h)y=N*RCnxPpNQpcQk zb`ptnb8ZncLaXnq_m;D49umd7cC7*2HeaWu@khx%3;gKLtFqF0XIfvC2w_m5vcPZD zp;(NTLY@U5C^flK*eo$N^~3mc4zi1U?Wk1K3J}r+_kuhnIB#(RnH$3!83J6YovyeN ze9~QcOl)sqy`SVTHTu4P;-q&UwZX6r|EGL^w+cY<(#8h(raOER?;mm26Oy&cZ9Lyx zjDl1_vnXbS4d8zN({xIo-MelkZ(2`r_J)~qHx_qLaRmywVP*EF2m4QLn4W)M(59(P;T0dto*|@bgD+=Ti!Tj4FdLDC z7HVTbltk_Kv#8OtV1oRR#qQn{%6!~Oa%UmC7D$Aspcg&H9m+s=+M&(uUR$R{k zUG)_L6exKtf&rOhfT+2CVFwh>%ZM+__``#?3p>}C=cPs*;f85hb=w9U#0%`dB#pE zo@t+L`yKZO{=4*uxL4-6ht8=a^@*T?aP4 z1!vgk<`JLu_%2RJbO5=jz@AW}o{y8kK`tpcY<@D;a5}t%yF(`|!L(Rgh&0lll}jPi z`Z)WdJG&ckp;B-=G^umXE+Z;>@nRiot$y=q7K{K8`XTMf;7on{cOr)6^_Pe7ScOn^ zMxqI(JdM)Q(vWBJ1i<~C!VD^OMO&tAYU_X(j^}`8j>OihQ%6^l9hYpq7$R6jkXVNd zx`6!Dk-PY9l7GAS8*=0>-h}exS_VUvRAP%-3X%Ty?fd)l#pDTun{$YT2#`wP1u`bL z25RcKC5(Vu+V$>MN2I+NqS7=(!MpBljB=3_9PIZ)E0J!uph+_`)!DT~Ep=`KA`@#% zR5Z3duAsq#G|s;Acp?h!5ppUPLn84%FEW$XO%JpOJAD8_Ut<9qX!KX5vwY2ai)Ny+ z_Qh(p*iDEQq%K)%@&U1cczE{{Zl$xs0I(foSdVkO=4Q{E-F=X-=B9C)p+0&Y`|%AU zumj##C$QWycfkpa^~GFJ2KH4{RQ$9gt=|K6DQW$<(1bD$BD8Vk3!w=RD3+h4Ih^;{ z#f(}0HzI64*lg&Xo7JBKBB`=}WdS0vOv*56cxNeH^s=VB=)MrumH`_XB@*(E%9Mq{ z$)AA_8&E%dvInM3^(Jq3)|jKBuPFPdGI9-CdQv>w*rBFmE6?SblG=$(15rGpjQQ%j z8}n-P!tBPXlviA289lw0Ca(#c{JbRF2qlc`&YYc{{Rdl&Ip`PYsZK^_V>t8}Sv&PS z>q`tI1`wIs^$>PoqZ{G+_Vy}3?|%LFNkr}u{X_x|^!ns#d9K?+W&2?`41S8Bv@iG>`M&>U z7(~%77c*y<`O{a*yfuhn=HBt{8HQ!V@#owiQcl(t7o}8h6^?Iu!r(6dUQ!7nnc`Av zzq?=$-!_J93t}J=8J-PA0l!_I`+?nIa=aCLCoQVwC|nmYy{T+W%~8#P+v3jr4q*ie zws~9rok0-sl3{+2q3G=6HYHNmgX)}k_BQhS`jN5tMZHf88&Ky<`QaiwtU~d=HQ!FW zb$R>wmu<%%%d3XuKhbhMU%y_Ctdp=2cqy4F3=YmY4HLCDbS3A`Iox)C*6tK+VQ!Olt35FOd0YoIMNnl zK*Nc?Bo#@Qkc4j%S)`ztnVWy6q43Wy{w$T=i2B*oFW~D=_kdWAgX#Brpx3a~xa6Tl zqmT#tEz&x6NoCvRA0@pqFmRh!X}9QsCqtuUE!jy8vqyn_Lj;u=6Nf_0ILCM8~iR$bW4I+hhl+Tf~>Geemj3k*5??OMn zZA}eS!_x0J*b_S@R3ImQz^I3H8V#5Q2+zU%8s{NFXF9vh4d1fyCRnK3(=AU@ev zydM;$#X22P2^qBByt%0~HMRVMKYVx?A9T4v&o$&V69{<9czWje6XPh#qi8%AF!)$< zJ{S5S?c<)!ao*bnH`GXZRVG3kFd-w?ork!3k&m$Dp35|TPsKtw87hOrl*zP|Ma9xQ zm0s)M4yxgSV-G$DxqVKjYe=>Hcu-RsL&`f;66 z`k4D(<9SXKLF10ZR;LD_kfc=G5f=C?x}-R$OY+MI>qqyS(!a z)iq?dsHCCtHoA^oM? zMV>F&0L9*MNIFJ8G8uz5TVrNI%DAWu!#YTFG^|t%!>T)I$h{pOq}jHnr5(?oCC<-p zndrKQ*|Vd}-HZx_^3PGGPx^hI=&n~8btU`v4Ad3f@P;7WApk^%v;67#kY}<_=ANUYt>@ME! zkqLx}EMeGgXMAYGxg0$_iFuW@g|SLnSB5BLYY?lG%yeBf=Sn>7?M=BowTVEE7p{gb z0Ptw4F~%>Jnw~nWn!#3bI2XFXqKvycA?5Az1N>g&0y$44e?)ouoaM`k_7ZY89(l2f zZ<4!(ttW!Hg4owFM$?`IN!e%53zu`9sy}HlHy}gQA0w;U1Ks|lz}jmONKs~ ze$q=yuG1}Kl}6pdwqc*LOqUBRUMNU=r}%dz4X+Ygq_dVIVL1tOgGyckZBRfFiWYk#sIYLX|AIA8=ofj0?Fh_9%a+oCyXu6umg#n-w{diTb$Y zjE>K$>xsK9@*3U;qfshMd-b>bqSZxc41Gb-sd{C@cg)18dfzBBYdMM2M-=HrwIh0N!W@PM&3@&lBpq_& zyeUe=HU32zD&+(2&S(k8H{}Y_<&C~ZH+uJM`aB2ZBlB0%_*bFf>B{F^9MZ>PJWF_E zO;HBwGXS(-SCS~YiAF`VYe~mYc)sJS3B^mir~PEI-W`ycPj`At{>26X7Z&P*-@49} zjLG1ui^M`mA1IZ;!47eLYS(+T==YUlC@ZE5>$>nCwT4!cIBy(tmj<8!A~eZFNTf-l zlZR3EOw6=V|0+>Dq<|>-jru*StY==_$-=^H3g_-_JQ3!GlJ~tI$ll73a6`Vg&Q?!< zd^U8Bg1zb5xtYUCb)wqvABf@RJWkzyOt2SSDlriOUagxMGxAv%L9Q5|8=eR|BMfdLvVGc|ZZG7(I5u2-$g{m}=$iNU#a#Nh z-PS|M#Wl*O$8Nsvlug7z>&b5CYN=G&4mNl0bhD|Cl$z(yVReJ4z_TG9uC8)wvn4Q+p-wOLE` zT%oyQf8xRC$5TAeF={Q-rQ*OrQUwyKkKo3k#iyp)v+Wx7L7Kdw@%QH@`btQSJ-Wj& z|HX!K>L!xVI2YI9$6T@Way&`2j5+*|b7X2EqVOAR%9BOyv*IdqQDw8b;ioFsV8?*# ze5sv$7^0g`N@CUGgW)H#W+UKxp@lCDGf5Uz^Q>hx6dB8>fnzy$CJwBkkva8=W>=ZSvOVr8Fl?(y2V?==RESW@vp)CnX2dBOk?FZ zwF}Dc4wESzz`$Fw5vT{D>USi(cog1kHw0<2ia@!C_kvECO)|TD-_5@%GY}8BU>ms> z`Jn&G!9n9Aqw!EyNq&h>VbqqM(QBd%ywg%a{{*{yH+C9l>{owvOE*)_2Us~dK!g`~ zJ;8wZ@53~#a=kj#0qD)Kj_J=Q!8=VWwY@FS7~Pqcu5@6k?-Xqx&U9a)vM*P2y|-L? zPzC-xOp`$f5U+=!KJCbi^OGv_3Q|sLJDwH}Kmjo_G3hcVJF}Jm9c+~}cq^DjL)=&n{3iV>U{Qe)DKoYO*%Bp?<-3{dBd)_=f)3v-v zAo=EfZlRSy`yaQq%nLt{sa8o;q7Cwzr2eD8l+?rSNW~w2H?3MEzfzq4!j%}U*WHd- zVV!=5TOh?~;hn*7wF4lKmI%~`fDyT%C_y$N8nz{r$DoUFH;8^IOW~_m`Pi}k-=A-N zG~i3-z_}=mOm-eF&FH|a4ak4=BVNdlrwAs@!m^xymHt9y->s`V1(#cpG97J)$+IICZ zTWn6B?(3w3%^miZ4!*ycpQ!Gau1XcDr}A;r+KrDH6TC)Q<74_tSdM?4jVpbUm@|2u zRCrg?)_w1zbS+~ubGKZzq)mL+Hsk>0<{n5o@dp&6I-^Ph;;dB;C$y=QLinQ2Q8bKa zrmqe@{=Q>_ySx-_jqK zUwS*@DtjF*yTam!EB0K<0c;%vNfBVZKo#0)^O(V<+dg|f{>{4x9M;plz(Ha%xPNh6 zY%F&g@FB-2TlU!M@^0Za$&`(O>`ijVIF+l^y;>pNNE+a7k8ymbQNtuNP~5fZ`YEy} zwBzS?ANMHV)ZqTlgVwsFcHNh*5rU>Ih%3fo!g)*$)exQ{&Z_u^i6Y$auhg0d>Egc( zpWbP^5EQqQIplc2ygZ(3`!G;qjC5nn#keiybV;YBeBfNf@W<4#$Y@_`5_n6u2ZU7V z>G8TVxSHQW*H3C>Gz3mN4^q3x{I9I+5Mb?~eRfh~=Q8WKkaw7ZTdwH>AQPA2Z zTlt-E>ZCM*SieW@$cSH&?9g?N@-&L3ucVK%hgyFEqA8CF#4u25nMHdn4}Z*m>6G1? zv0K2xG6;T%DiW2##%~g*K72BzgVdw{5ns3YB5QxL?da6zB~Y+lG_Qt?Xp2qE@gqc! zL3vzTr2%N8Y|u9!p3`aH!}&)(Lj(UPxDyYrN7THPHM`O5dTpAoJbO*z)kdy;6J(wv z3z^NXPJ=n%>4+5_&*qXl@)Yj>6d1c{>~Z0o3E@2z-6V31$G}k8zse1GfNfoE5M|ex zw#yiz%Dz5fWjUs|tg=LZGbM#8i(Q9+bcOuLMy#%eS_U^w`IgICW>#vLpQ`9%5sf~4 zaFcR^iHc}m-4}F_b{cxKCcY~ZUbRJO=dkS4F%CQ33dJh}#KRRE^{@YE9S1`EyD>(2 zvRdCj2-D5YEB=}eCd$Y*FjMO#kuLKh8}+a5jQVPyKoLv2hc!Bd@4CxEv?=5)<|Scdekh&q2KL%+S95QGs;| zN^yKKTRUmhOxBosZVe8UJ;oIPUaTzlLna=v)*1BT;3OIb)_MEo{UoHNL2UBvWO;hjQJ*%FJ z^<~vu+<91weWd7N_-n@JGqAwB>x%=gKRi)?a=@G_w8zx)*9=>#m}fW1BI6eVUxbDB zH*tb3ETTVU-12rFC3?rG8f(%p**FmLC7;9n<;tL0pZ^R8JE)A?8sMs|ie#304V9`I zM&WrDCl9Dr)?4qAsK+YR^^X~ZLCT_J-CEu^J&^1`uwwXK1h07TImA$Z1F`g%8gY*y zj$@#J73wKz@Gc5v(He1-en|Wwsq{(vWPQg1ha9wzTO8{$_*pesoAr)EUbX>w(oM(5 zr@rqswW3BTPbJ}X`T{(P=;vD%llzZR92wInVhTji!i4pmF9_*iW6{%k@DmIOyQM_I z;DOll=UYaJjMmhb59n$}g)+h4x6Iyj5ufs&X{kbccN!CI@UfLfy}TdUXS)d)WPFAQ zq3ShSo>Y`eL5Bx3LrU<%uQvyCSwGvfSv23Ainln3hh}V(Fv$lq251#a)hOIks2UDW zEG!s*LJy5m1JX^J=^B?_9hpdtAd&o=lgG6S`?15ahaLc8w`C)EiDAk=i2mgOh!j=x zsnAVVV0e~XH`mCiC(Py=Vm{2(DH`&p}lW?Tuj!l;+`LzP^fQikLaUCYNh~~ z8dWTw31m=VGocp!9>&W>-gp=M`bZPye+8!_4X~a|P4xoZoFLNx+vT=-CGsNs+cL2N z^(!y!K3rg762zLT1Zj{l^n@w{<{j0!#Nbt0(1t4Xt&Aa283qr=lpS5UfMFeTBC|B@ z3#|ZJkoCby?o2(Ro2VRY3m?8#4Vb>e;C$yfO%Rw=@FAs-mM%(8 zJI>WX1LIOb z>8zX4k@h;P z{A-~0g$&kbL{-)q1sN3C1_xL{*%(2oI(JC}DiqYpywh8q%%DGA>Z+PKD$v@%ogDM@ z;=2s}+mpDi>}ewW4q%VRsev>Dzsxur%eryCrAQt6q-akQ=&NTsz|3ZUyNwk@)(JuA zYRwiebD??<3ny}W<3~i$IIq$TB|LWUXD~hwSy86FzVN6ag<)mBF^s8aEgl8473lyy zb|~BFQA0!K;Up4BDds6Xl@iHUxzg^bmjkH)F1)_}*YULDWFi_E4r>u<8~8PNN6nQg zi|+TaW~8w8sC6HhyGOZH7@0e63M~9nj}coTK+@Ci|6ESZNR$VoZF`0h2oZ7eH+>tD zuLZ{XgL3)hUdrDUxw6W&!k`fWKfvm4gyl=~W!X-~P@722a(u ziNrMfMyMtx;xur6wG%csWdQtNtxCMS`KuK0s*$mkSy{i=5*7?5)iL~NCw$su%3mi8 zKOD{UzV+~b_r<&*1dE|O;X}Vgx_t57$3+$5BRM8P3~TuoDs@t$Vp;9ugau;3)Wp5w z-Mac9&RS-B0k5? z4$(b{;sdPz6LHNV5#vK3DU4{nZaScbDI+o(>PF8z{`Rx7FP^mjbD`yFpG{_dZp0z%*I`ZChM6gk zW^=Nsor_bt1M3e}O4Fr2N&Eu-Z||iI$mkA&q2u)S3u1GVKYs9e%hGwTP%*D9KGBf3 zjXvmx0a$M9DK=|sYk#=~mMeR+h}B8cvYNUdiga&=S)#vu>&@y(7jgqR{ZOs7F&`uH zIsDVzP}I^g|8q-)0I5}P{zGuQ&dl1A!wOW=79EW~f$#T7`^bMrJU%L|(Nu(t$=#4m z9b}d7Y(yu$da`awYWG{C`MXi_?M&IJl+E1uK|UwGv8|LXU*ssiZ^|9HeCB1egF8PsGPsE)mAz|MBz{eo?+p zwC^rUHyAWahcqJ1Qqn1cfTX0-T>=Xth|(clf^>JYgtUZo2#b_RckkZ){_g$U{SWqe zpJ(RGnKN_Hb~|p-ok6iOo!YSzB0oQ4CcWkVTp(eRklQ^y-@hZQwP}8g!Kf@!WqlY8 z?z8&op1c62;vYXkX4`h3*eLZJFOImd80hxR+DFOal5B}h2eWeV3+ZbXxg#1L@NeM7 zfBVwl3Q&m7Xg;3f{q(F@vvKI@KJmF$@aYBp$RJZ%I3%lU)$89;g^0q?Rj~?+KKVF_7_>|+s1)hS1;K%!+xA+Rli$F0SX@ zz8d57lOG41@vl-S{yWG{1JvQ82M511tyUQ(wn>kU$-uJ4fSSsuf?^0n%+;I~@1*DM zS2@~TH{XO^L@AIL4%28f*l`VV^(}Wy0WaahNnPc3yGs*783TtkdLHZ=8}ig-Lk_EnE7Agz@+erD%PuA(4`IA{zl0bAHu0nCXfwn>?w!SKTiW zJx(1x!zFz%wVe}zRP(cCC-R zpbX!vi?>6!lHRO$2(@Ga>yooTbCe!q>=X3PclpPsr#N6WYmCN6Zr9O`NHK>z<5FA3*`*V1~Nuzq)ah=ucG9a&+0;X<{);n__`k&cKTF<54< zF>f6WmFT~`<|ERH&Qrir6BDqt;T77PH&F$i(#;xrB?Jo`F_uxpX$|pY4gMGHlw3A+ zEqZDWYSAOZ7n?-8j`eW>=7?7F?_Y>i-~W06vy(s~B(Z$YYWSw@|5ShRe|5GHPCfVI z2X@;Vg)xFlca_(c;2#YYe3VJzW^+?XyjpCtpT7?k5A<7Yywg8)a{A?AcV{9}O^GA> z2JcGy_lkZVBzGLxIVBPL!6<%9x*$db2vz~96Z(){-N&PUl=4PE8C&Q6I|ER>8iVxZ zXZ1f4Tsc2O@iFV()TIOO5^v7uwiN-XSFyqV2nL|{LVopFF^%YHcz@{HO5vd*S!t$l z@P*{>Q?mu+AoXgYK6YV=4lfVinDF3_OC$N3?wtGvrbV1@bU-Of)T9V?e7{@5r8ifU zjZ+z#z9!#PRo~tlj|E|xJ_UtLjFBJGbHr+4!ro-+IxH6g?R!WcA>KNO!e!rx-)qE_ zOYbvoFt)OEk7!rY43;0ZPOv)H^vi=)R8-zOkcMR7h1^YT86eCf?o&0-ZL#q=g#HL@ zNwCC)(r8<^$wD$Gq6GWC4XT|K?^-~nG#*06C4R^i=;ksFGO)SxluLH3OKpc2* zcEuXb#-uC${0u2&nfUa@B*w(O+T?C(+w?%3)D;pJHA0DP$t8D>CzAYn*Q{bbuY(yf5z-`+re3bMcF6?cnchLn#VAY}L>oV0i zwM~x*E_}f4ZBPSSYLbZ#NAR_WE0a==9k2+;2!(HcXZzacf^l9(0k<15!SY(y^B4v` z9OJ4pX|xl;n^x=)-ClLCtuC{?Qf2aX{`$H>Z%+~zPDp3heb+_dLlYOQL(oN zemR>i*jHBFmI>V0?#by!|5ZK|I7>v=>4&}ejPU0+^x|I*nN+I{ZPrvVhnexrSdP22 zGz^M9%N0gz(o&GrM@FjI6b)K*zjtu(&^vDpWCtwXgn!^6@fIuoQyqJ1p7rxs9>A^+ zC4_^ipSQDr80Dew!R+ueT!3Rexd~a-tl(nGo31_+_Aktj!l^)CkdmZ2hTi1}_wT_b zH?$s#VWzYT`_qs-ST1vVnW!xL3|#1^@gx5@Y_ojtZ@?X9QK+3AI>IvqBKs3I4Gdb?tYg?JFfUM=f1)~`AWq}kSXGP20&Y{}MD z5&JCc;>-7zm(UodiPAQP6wBzr@5?Be1NQy+dZ zZz8}1ri!&FVWLs%lg20TOEpB$`atCuEi&lq5370u6mcL4ecVOiky44|UXe00fo;Eo zDH_?`0X0(mrmtZ-6@3qgBc2;0*qeR`Qec7;L=M-)KQ?k1L#iQy_lU%#mL9D#kz*PV zOqhBR4x2W9h2|`F$-oE!m0Pl(sx5c)U@5* z78Jt?*%^{p!as`R1@Eo&WLo`8cDt}UU5Ct#l)kj{p6@MKosW!brvbiqQ@v_OIq|VC zA8V(7Q{2g2F_XN!W5gvn-GZ~CTVqe(vmIzSeoC@8Wjw9>jg=dfKW}B6Z^&H2`Gwxvw!1#Z zIb?zS@h_(vtnDCd>2++#WzEMZkZK=qy^ zLX^%H(?MxA7ZqC1#D3K;9+L6&iQy}Jhk5_u#v0~RUhuW+{(%SMuJAC*g)%N}E#1^a z;4D2mZMY#ABsg(6#U!dWKh&miA$`Va=PY?)+1M3#&98?uIDMZtc2zoYLFzPlF|+sU7m(ynZq(*O+h$tAEL1mBID zhJ;)0A9zr6y=|8nJyvIKBPl-ZwoAYD9Y+M5 zbP%R%n<{6sGE5D|@L>3E-q$W+g#D`MqO4OikF-{cGobX~KlruYCH~dr?cgF3iDOlz z>E2|E>mh*=@pF*g-CI4F9-YHj*WRCMaQB`gvCJhgh6EA%*0;KCZPfw;6(gV9U?^T7 zHq09Bk+wz$imP5*9Xsf}xjL@9xq6!+X`jn(?3NPEj(+X^tBJ;^GK7&By72&3`O$2& zFnG%3k~AgQ{4~}en6^GB?wmJ4t=x>1+_bLuF~=kVbr3W ztS8$~`E_uR1D>p(OHy?jWFGlCX})knn%#W;n!1Z=$o=ab(7sy%%_u~?2V5WG1KR8= z;!`?{%^;0G)%YrKN`I;6T(a0lbAJe-|DDJq9C{NLV~ygf?jCuU^yfG4mDB)CB4fuv zIp|M5sva@}E(hm#@j=vdNVh@kV!6V)4BKiURY&RDRJ(}Lc~#v&b`o4S^BiKZMb^fs zHbc&L<&${MRe{33tWH)vu1)ajGF@dc05pI2w((PPiGXBx9K7xB92f#ceXVM5?Uo(F zqF_X1OwEUm-_&k7wr~rM1?X)QWUCX;Vuj_cA6(Ei%^d|TA{(nYw-Iy$ySIuS3~Y<&YUy@uZu=ib%DD)sY5m-f1-^s zy-qyVrB8mkpPGlWA-(l=o@fq8xXT z?Gf>zz%weAsE2sKo*>3lSB#D^>qusp9xGG}W4@I%?E(9dQguj1WyV7agnm(%n`$~Qz;;Z$rCvI-8MpQ9+bzaeQnbm9bbxZwfByPOdHV`5U zqVwnZdpEF?-*}GH=xJVbU8NXBGoa?FNyAyc^u?Yj%zd|bjG`KV9LUDyZPM1F?FsRa z%NsPua@QVNvtM2fRa?VUsYd<47N2E#Y8Q4L%6Kj#)>hYZ7~&`_q8C4o5#RCLy>4%; z-m{hZspo|hO%V9h6vW~;B-xK=^-=J{KRhG1_bw^ZN~xlp_=xTJvXZ8C+r#HsQu^}G zlV61+J(dqKL0gtW!KR_srOm>`{c%*#!z}hC)rJz^-gU|dO_H~nmpg7Ll^vh=y5PYW zHsS-V^8eQeeD zgdk2$6P|K7IxEeI(61M>&ws#G`28tCqSa`(JW}L|Vej@|WRPaUvR?}hJ;aMo3& zahEGF(lto&m#HVw(L%+8=ClNsa<2O8sSop$SiyAJg4wPk{dueDe= zGUv(PvGgle!VH_gLyVIH0NE|#+P`79xjCikVE^G6)NOz>bGe|NBsM$;_gTJ!FgmV; zc3A7lsBU_MOc~)EMkMVw)_`_U#IrCnh@N7_Pnc@49BwPMD0ggJjppCy)p@xe_YTs@ zt;6!xc$?wwwY*44R!u3vpq2UyLsCc_HB?`F$noLrCiTzYQKt44$)`$b_AWxnuK)p= zyxZWlQ8a*BOxzDneJBphk%e4X&If47^bAuWOWV$M-JbJ0H<2fjf z1=yl{v@<29aDndj;`vfm9Ya)v$pua37QzB^u519UbNBac%Ym`y@0yJw!Z1IYIfiXK zG0E4tYED@b6#U@4i-+HL80T|fz7o0; zC-vW8fY6(OpJMVB1jApJ)O_fHnU~kLJu6#snvI!^U1yx2Xv#QIixM2G)o@7yY)e%9yQzs6f=;M!?%Z7|8UzA(hxZHXHore#>cupI4z)=74JW| zyL-`rK8hT8u-+UItPGL;-d8a8V|NifbkP;*5I=v|A~L!E+fQ1v7#Hr=tRT5eF0N6# zrUn><`z8%p5P+a=88^W$;7`xsTDEn_XCv?;rrSi68Mpd-?`b1iLlKS6S2NZBtZ z0Ojy~Ap38|klt`Ig9m@Vu?<>tRE2|VrQK>v1I+=A?1=gMW*B@E{|@) zh0QKpjykL_zmM@}5M>I4U;Qpllm5PFR8N@U#s+xE;RA3qQ9L#$E#PMvh_z=k6!un` zVzjUne3wx|5phv#vEY-CG#jp6Dghlu+FIq|AovT=_KXWwp@T^u*z^uha)My^2`IsF zCaU+9mArLlXw&uo_oL#eV;X!_GJ3Ki{dbZ020)Tq1A)_I;#1WHol}4YQ*;`dXSvPe&(aGK|7z+T+v*4LNVRXk5;zBG2ZOxb{lBY zIHrf~FS_r+Oe@>`Koh8tv&a}&L92;5V-``$RoB-esDBXun~WI{c_-bdw(xx2IR3W^ z5a%iF_78`CIxJvMaEBEz*L^B`4(IIo*b;i(D?@I%6gkcKty3v?1(>Dkk(EW zV|Mdg>s6YYo0)C+mEg2SDt=u|5K54L^|82qilCp&DpK(GD(ug3&NrbaWO1qL(pBvE zh_1@KmQpU7?0-=@J;u$}?o*Yq7;VC=68$Pmkms7_k2xu=7xjLg_M>d~Nkelp!f1f=b2uf-Rp7&jGgSzRtyV5M z#qUz3#jSgj`76&qBQUm7`um;5Y(7sA+z(88N^jxmhYy^0`lI&nCDE|l8%**u1=~BN zMXE0851UCWKOnRERMVIG%>xqfL5yW=|A4P76v%Z_my>p_dZ!F32zzCX|FY>>HG8ge z&kdZ0i-mtA+4dU)^*=k3@?U^=y!H40uzy!b22+OWRN7mjB`0f+gt)lWOa+JMg^zLH zr%vWU00h;7yviobI)t#hrB^d*K%aLtlQ$lv+r;7 zc=E(a;jPbi>Gx6DaVK_(No<^ioFAwY&A5d$Ksp~Y1iw=04X#Vad%C*zWwUgag*iQ> zDGCd9VKU;sJJeJfB4m&GHRt%+YVmvxzcy`WK0W^HXd*Y(B0e8J=dH1n2FOBDjR}Ha z#ijpa&-huR8k+EpC%m&rb-W>~;Qv<96l_SC+x>wX*>mYu+{i)Wj{HXke@WuvboEn~ zvUM}0PV_}y<{yxjAF-C^do~z;i<{YNm9U3Hvus41Y=ujohO}W!;XSmpzXAfoCoI&| z$bhihGVN?E9iyvTiva3H@7GU}jn?(FWh04-`vc@3up#e0Pqw-S*roi0*qIQ@Eor%a zeu$-BK!7pY`lB3&m2ANVRq1hiA`?>u*GY-N9sMaVhFNpEef^!6NwqFjcjE!p*tF(c z8JTR6DRj>yth8@pb=Af2`_SIr)8qu5teXzr&LcshC&wbAAKRz8=<=NFa{T?IV)>)L z8`l*uwt_7xI`aev=8jB$?YGzx_2aAlQ>sqQH44)gI?oCn{VII!8<^R%6jteXLzVTa zU4F08L6aFH)InhOOw^-V^UfFz?Wi6lkby1sx`QqM&^8HZSn4UA8k^87gFlsoVD^cl2_#Y|3FiECF*xyIKk{(aJ zD>}>gMjzbRV2>FU=1Luz5(o|WWyo!O=)%Hq-SLTEFDE64zV4^%15g6lQ9#8|p*K+E zHYj{^d6^AR0}Z2CPMd4nM^CJLC2}iXYnWTfzPMlcSG&~t$gC~FT>7G6)h12#;u93R zA3It&idOq?Pij69j@9O&xtYkDe;i;0qn*&4U#f}x0?)2A4_k$<>R7`SH>wE)M(TS7l%F@$4M?yVs>tKP@m~u*J*I41|7U^KFP)y}C8R)T zJN#Q%TJbZ>p*@D(p-ib6mVFyU7#8*3d91NgL5VutD-2p8XJ+AVaQV6K4rI{w!OyHB zH(9%tV^w46Eq6Uu+FPn1J32s#@`?mA=f=d_75!_JNlh4vUgc1zDNMW!NB}7UwfD5U z4y+hb{FyI6^Ys^(&P4B){G84MBH{z=enm002q017^{1rc^C}!`uyjeCq5_ z*@*ZT9nF?VAz7r;gRmx#m*>0(Eu{E-YpQ#JixVlM$4l<|guUu1IFHJp%SuDAk;*Dc zq(5`1)c88NI$CT?WI=qm|8pGh8(1xjef(o=y*7G7`eYskyXYdSx9LJB_#(HM(z zkc?nBpJFwY=Af3y+;0%BQ`1OYFvyps!sRVJF)DF6rqS?%EiMnedi6e2gmyx1b^py$ zO_)Q3k^CM)VO89nLx4D{{EVuC__(7H1>5={Zp^^P$5T+CwdBQov@k3dEHMRvD$7eW-Wg!nr5Qbd3xNgEvU zSBwJAp@x4o_lAfQ0;gaD5`o3@iLm!L(T>~pxTUgO%SYLO#w-{#A*rsZ zQ8U`6Y*6L~fbnpTQSBSc-_7m~6SmeQd&a@R3*OZ8+Hyf8dvgR3~ zN}8f-VnhYE%fAsc(^(g{swd|0E4~N9$pG;?`8o2yI^8-C4724*xF} z8Ru8Nfc-+`e|(_XfN?D`vt*Rx@$4+lOdQOjg@dFjF6WlVesu=_Ht3{*1C&{S;*&jk z-H=WgP0{hMhsrb_G@6K2SnvfzvR+f8gC*Nze>f$JW{*jOib+WsxU6>CMIMaIxflgS ztsKU&xHy)V6k*n%oSxcc#zV+;9tvPC9VkbcUsC{J;cXD2Ma+l7F41~n zLd6bNWINP6;UnKxNgqzM=*vLXJ(t%4k0DRq7>|odrAQ9H{$8XOCD>16HD;yFXrB0u z)nVk_yOP2aZow++T78Q}F6-94FK+Jc{l8U^DVV(Xi^2N^Sb)e!k)#Z$duk@Wkg0u!$(C=4LGj{05f3ks3Wk4>rmlw>n0@z9@@3o8zT6ZW1Nw3;N~?}$AB zE)Sh*L-cd}KdlGAetD_nC>%s~{<#QPe@bb*efsG-#QEh9H?Fm6m!!wH+t3+Yd(mj_ zE?`&0RW*y&+xzkpo|u{{#207uH1PKuAzaal=VQMGOC$}xKGxUY2}9qn&*8363fd6B z8OBfR>b~_Y*eWg{1TCDSZjL_!iw_GkJk40&W=YU3${FV>K!z2YA3)fkY`>Z6`M#2> zEdO&gUfYZi?8n4i!HZ^GsJ?XCM)~hruLI|v*x%Zkm5y06uo#V#3bhU}uvJik3s>b< znT;Grkp>*s^m2QUwDC-e9ZucD@?!bb@Bl6>3>w$`Hd^Z0BZ&?0L#@$rU@1kActO$uuvE%^gFjbZX595ofX~7x8j2m`0u9n2g)=B!-;$g3@ zYDOp0taUbKu^4q`4(>euQwV1oMdI;C+K7u7Ir;FIEj1 zIn>MVHVt&Koz}_~FO4nB1-GaIma)oPo*1VXT7|?W>s={g4d*eI#0(85yi_~8ut#^b zdSVvIC#w5ei-l_Z8Dj<3ALk%E32ZIW)b3t<{Ky&xP~8f_39w}chS0D6lF#*EPUcZ8 zCXZW}p`Sd69K;I{^@9h}9eH0~%wU$5U&np!&6aD7VErLB@2~gN<%HD^ark_`xZbY$ z1otG@D@TF+3YLP|nmPD)>i)%$2s%oyReT9@143nZqT_~^mLzpvXYB4>!@X_3d^z19 zuI{5}8V1Wi(x_MdeucdJ3KS5)%cdb@j96!^A@~-*{j)y8CsU%3C|QwTZ>Q3;b8HSH zL|OrPr`N5;o*taLF;KVD8=efE|D_u`t6*2(K~r|En7V}0qo8~HBXcErf9-^6NcFY` zpo=EOc(Mw`KySZ1spt=Zs2qRk<$F>|l>49y@_ho+-P7O*t0p)+eafWb@AyZ(ig$P+YfsOb~qPfMd`(chzZJ>Ad zE7TvUNcsY|U`4`oWaKOIZC~_fg|A=J_xYhON5YA$qH6k6t@dhiganmsUUF15R|Sgq=cLzm&T)yqzro7^-o|lq4#O1IYt0< zRGz~1WO3vXS%I*_fw3ncXKOywe`HdhVnfY~JjjfVcODo3Rd;svM9j4qoz*($VfKJ{ zV1pVm4^XAOz6(r}d&#lZNhELPNpQzxwD27y{Q~{@c z#txyO!Q2PT?+Sdhx~*Ve&{J?o(Gd9>+a6lv3&(vQB#S$*SIZH){yxc;?x*a2OEWgn3&if<>?lzt2YeD)^8`IQn9~)I5}QU{G^|pu|XgB z?K164@!&EkT^5-Fd{@mWUu7Upw7WR69h@QRa`vYE1|=cSJWoAICokOIz72te3kSXo zWvc%QS)T(oL7vLmZ~kv+_+TpZw@w&;1INGM@9TUOTW3a^^BA{WmchO!W&$%BHA+qvq{X4zd zjCd$${-d~FUtH&RtKgt4 z=Zj5e7w3*^_fikm#8rKc#|SJK+n$BSaRuMCR~)!FmEK-9O)I>>UCxiy9WW6I{#=;Q z0LwQJ0HmHVAmM;3fSb#~Y8amO3?<;W250#YT(1186!Z|!`Y+UdYkRAh)nf75N3n?= zpUru@OW>;E(*TWCOwE8aq5~nl#zz~;zwqgWzAC-@%e4{wxyIlsMEc;YvQpe{Ejt$E zYRj!dggJABQ7eLf49h}|_^p*$Uieo`o%4s8gsR@gr-TBG;D5#&sEPp=tYET;M_NNj zKP=o=U#k)bEF3%(=qbe%X9*PH6}Z~qofZ<3KKHBJywAX^jDy!bd5*mhR$>X@%*%EU zyF}6$w`YF$en9&j>Icv2=>SanB^u?@brP8UuUzo!)T9rubcfb3%+UZy`(9G*Cn;wMdF1_JVvzb zylO};Z-L~V8IjSL7J-3@n(ySQ#t!CYIZp4a;vMPmbCWJXpRTh%w9Ba@*lh}E&rD|S zf4NKCyi=HQb@_q80#1_s-ldI`mChejTU}9q8M`9Fteq_c*QOQ3v~JHQZGwrUI6has z7FNs#I0Jei;c10#E~^tbI+!jXzKxO4a?aPbaNW3s>l94Rca2MRie!K)>YAjJhgs5% zmd25bX#Jyqh{&e9QyO7`#sofTn?B9bzG@!O0Zb;PIq0P_AVOEl#NzQVy)2SM>Ay69 zo!?ym8U7E&nPOXXZvqZH2%zos59FANQ5X6K3nKpeM{dAZLFKk#o=e7J?-L>8t0vb6 zUkY4Gz(*VJ-0rN3*pG9K)9#9QjTfx{P1F7QnrhfQhaV=6|986;zZYgfvR#!Ko)m{}-Y$!Q_yfPj?EwOicT?wdcq zX!@^n$g$5iDbBGQ{czEPHs?G@bvgcbzgu{zf3J=ZHncTWp?t5?ZANqg5RAx$shsH# z0k5s!H7Zj7oYe^}arW9Iaj-#@kqKD$?n3?^UhrJmC4W)-S$8-HV_-K9HiZP}OjcW$7I0+&-hkl-sHF}(qViDc!^=2-jMC$E` zXR97d0@+BPPoF#{4@Yra`F#0aPAHq$xze1snS)6+VPT1x#;SFtqY&`l;Jf;YmG;0Q zv(BJ=v#y|`6}=HLGVlOqVgpT`{_SJOK(wIyUfJ7iTo;NWuLNO-GaEW+*o_l6OmlEQ zM+Iz+fK5sv<|ZEkmyE+eQm|BT=>k*;l3210{}m!`oMnvTpZn%zT0&#a8D?+Dy4ik5 zZXa9LRKA7lUp0`J53Nx$DcmmVNen$ct8#uQ$JR!A3jZ+w0={X*X7&2Lj8d? zq_#zzh;m4W03^^TSI(Tp#Hu7luht9AxRK@TuZY&@*k?&(%b^EEc8EU{rmNrKB zM{zlsi_`u-BW#0O1eF6qt+sEL!|(u%cVXvkoGps-(!+5Fu2wfhvCi^pix44tm8$*y z-oZR2EiA&_W}L?Mul7J(K9$sHV@fK3>Y~84 zW#vSj>&EoYXhQVb46xXpN)NDqNSL1>rAgbr$sH}4n|M)_s{WcPrxr__o&DY_&{8i< zI-^COyXkw+h72~u2Lv>iEHllFsNn-DLYbHsLVv_>&0&97(6k4p)Xwp-5Ak%C`%hA= zqEWsfc}#+0E5=0z`}pM4-sSLIDQ}@KVJ#Qv^#Lbl91<09(2!ls*Io09EFbv&f?6%w z>K7Ma6kHl)2j2^%H)cKW@;2{nvXr?~I$b|GN$+6RVE}aYqsNSY+J?OUmctkb(>0qv z5x_R=>@Ys^Qvs~au>IJgDq@w3!Z;m5aGLWqyYE~)6B-2s`T~*Z`>bx4tk=$pQGK~P z+_7D4xL2u~Yz|q;!#*Dbu+O6ihI$685}8m*6iv|&?xtoipKYKn^lbHkFzOo^lYPI@ z31{6g5?T+(1Db;1Bn!!AiV}IIVY*ZS z{>HP)Q+BoM{DIH!nQHmYyYV!&KW3?{_BAQ91=FWW4=%kAS=Q8tgjkHkZG%~`5QSLi zC?^6NpjPoCkeZ79hJYKOA4bzlhJXC!ULkCai=po0dW(m~l2C91b~H>zLdypI=q`V2 zl?Jsh*>oA!9Xx8JYkrwikftbz(z7Z;)FCIHD7`;)Uy_zh6ofVmuq>eY;z3YC9Z{ID z-ZE^bOrz0CKqvwmyiZP#|9VzjTllUDK@BO>bA$9B%WOv}`zDc)*O zdOD5^RhiwdOVYafe5kJrIdi;SA%Y)P>&l?m_peeS#tWY<*W|DmJtitceO`MAW=w@c zijA4wIlF`pB17J`gL4gWbP{&vYTZ9#D$66voM+>Ouc?I3cA{9o>9up1D~fj|Dn8Z4 z-4qOD#S)=betCAA6Ye~p$6e?Rga|LiU}B#LVtR5oCa?F|mS)l@`~ zrzv#?-iGXM9gkNQ^B`|-J`aUf{Pf~ukJ=pFS9Zg%BJtNIn*YB?Cw&_3i_44{5 z^Z0Gk7inKJ6W0ZW8xOA*YD6qMLgV8I)0_?M4{PO=gBg)Dk)CRDMq$vmVQPQ{w}ihB zWy50k>v&+^JYwC&WdFVDtMGIkfcnTs3w*ZTfQwuiJ<{bh5+w zHUf5!H|?dYOQ+IId645A-8afBTuG6C)wsKP8pqE9+T7Nk8oHJC#c_Sc&+|z}p$Hgu z?@@H|_gAZ^s3f8~0;yFGSv)89C%+=C+8I|P%GGHZd411z-?2Tt2{@6Es_cgzJT@c^ zzPnZnTv^GBdAxl`LtW4UDJtQTAQ8u7AbsT@a@=!Y{hc<&Wb8xi?HS#Hsj4s;+ZJHX z)birPyYx=l;HdlWv}m(jzT?YMJ`~0E{j#+p+<~5!=~TNk_mPC{#EheOuSOd7YApo% zL-gs^6~>BOJLjTBu{s)~EJCW3kNBImZr8{bF8@h&V{CfaKpI(Xi@__2$-TS_=Kumg z!CnwwIy}1z_<>|h)1JOlxF9`>4JOM+cl@lR|?>F?-cK$g*KgxeuG zf^gye^r*R+>B)Qf6g)U>oKjt#@Vg*qg9cTQ01WoH_^Nz9Ne3WWI4_#k<_lsfT z-Koq(LF`lvbwcp1F@>xokML$Ky6#||J^FCdv^oLP)?Dw8u@wHi{6;pAC{<7!Sf0j~ z^=A4H2*|p&ev@WI&|=zY(L?ey7pz<)qs{()@(2?lqb>Z%ULi*NX$hig*`f1QOlpPj6vvj!#zkb_lDR{ zO~Xrqh)zd2*6;jmNc&x2pcj(kg0AL)puLFEZinRT*{y7}2r#>|kR~BnM?I&PaJB9s zMXE#E^M{VwXb`Q6U#%TBKX$-C3oy_zOwx#3B5?l5zRfz78$`JMMs$sp@YF}@)oHbx zC5dB1jK^N`8@P$jRJ)`yc!^dY4*^l!Pe6a$j$y?8=+Wq|^OR5d%>@v|^p+{>%k^q7 z{DDjys&|9(@h5=q;gbwuE>z03r@F>B(@>9L%M9@dufBB+Ja}ZO)r<`;Fc8|1br+n? zux(nQaf|bxk}<50>e7vWHet)5gmkFd3Da!^(2}JdL1}7eAMXpj1(@;u0>fY3bj@>F z=20U2s~iorAu;A5^}%CtW1x0<`8;ivT_E}eInE!5_fZFc@9~lXK#(bQ*$JPM|7@8L zG1#yDX8vL-_`T=13;e#r`0`*Mm)J;Nl{=ryOX_)++gKs&JIy#tbBt1A?}|De5@+{; z^+;qzCb^EUGkN*x1_vUhTDK}NB*t6(Av93zfr;`w_#i+3Fb zglWNpIiOz;=De5EwaiZs(Sh+gWJmnj7&~^^HovL7C>Vl7O~a!6a@J<83eV9>tAdBW zWG4CH`O#X(8@rsdNieEyE^qx%mK_}jhpFF0ZpdrGqIQ{I}H{}&UQ~blx21aSE0Vipg>KOiA2A?1J zeWyVcJ`ZMZZwY3F)6C)`w;kc!Z0#5T#qI1YL?_@^<|{b-!xe}9yM`T|M>4McBMn7< zMx=(G(T8D4@ihmD4>wSpozLehdfIGcx(9b@B})Ri#yJA2ACARk#F^e~l_m{R?bSlv zUZX$0(8rUG_}|^Ito`>8;C8BetM=M#_6G#Dd4D2;Y@-hR(5C8%M61|XB_Qwn1 zbi-Zly~3IAx6DkVeK01}g8|T{xPVPh&odEde45AJ{I}a?UUv9@Iq7TQNyQ@2FV7;f z$vP|M3I1;{@O{zEc2^L`4qDyYNtw8Q&A_6@!z~7fg7U>Fzu+{btX(#62OEXKa1;4b zvJggS^R9b2j6Ml7u=_Ij!yRBDP~xmVrR{ZByj@kiv+W&pR^zgRn@hR_%e>RYsa9YE z0P{JW)>H>vXBOviZ_vpX1YrtPvF5Z>hYLT>oKOVQ;hQEf!hfE`?JJvf_7^;42l`vm z+TL8s2_ic}i#!KP3m|}1wBW!zqZk;GSUK<0^0GJ0a@Ml);mDvAmo>A)rv+v(@Lfva zIx{QHwu>|l9v{<}aj@&?|M|*x(lS%+{kmYnS5Z4AfIW{GDg_`v?>zdE&=yV()CyoO zN59yuwb*Go>xb9$03`zD0zsfK4+ILm2(9}Tr*XN=l=OWQ} z(fpiMIz)U%vv6;2M|?0_P6D)-%L<5K&_5}|GVM(rf8f4E2z#-QR%C3st$xGzv3J z@7kta3>=x1-~#-jeuvFg__A$~RnJVcQL7bps?KwKwltm`V33O|2N#%9Lk zo*2cq>HANdJxnprU2nD32MUGASFm&es?iQ^>CKTC(!vHQa$Ax?XY4Col?uSiu%w7X$yKvx~@niL~YdT@CXA zD9!dwfuGC*V*-@2_?WLB%`_FF+Mn4b@$q?>o+FO}Clga%fBhCL>Gsigbg>||y}v3P z9SburdsVe6g(enN(*%-$MP~|(nHDdNExgfh2gl`%++(v2x}TsY?(ky%`S7= ztBezbjCqskxmayswALho|HGknY&YjL#rtkp$n5R!7&8hVdj657xqU^D%N7|0r4%G+k?mslnzS z!MP6;#4AiPq9`8mx_x0L_~&FDkHPFw*dSoyBd<^lZj*?Z78WPy3R6V~)zzX&4KAu~ zE;93|sXj{2%>V$+D+*|N%2=COS=IXt&ekHkoD9K6{mZc{1`8~>!iuACE;NM)*+;gg z^Nf*=>j)cTczQn}cnzGv0$0|QtyOi|;iEe{kEhQ>GmEg%^q`}ra0hAu5}Ub@Z6k?G z<>Ty~U*e8ZdWkfgCs$?*T7a7@!_C+I(76M(W}-)49=PF8R=sN-W;jbi3c0+7t45MW zP>-6F+QD!&z_@y({DK z8XBxmDE?asqOfzsjbE^1{wG6Wv>U%xFfG=~zqgb}d48(uHhsk6^Z^ehc&0e0g)J5V zj!sUqD~H69EAB?YKs>>QfCexml-QpSmo)q@MY~%V`)p+3*v;PfWz6w|m>Z(dc5In%7KDzZW*unh%MG3>0fyDU|>W(@abi%_aCj*T)bB+-P3- zb-j$dOv!ptnUz!JJ*#P!5Sm!J^Os!q2L3T8Q{)mizDZ&qAw~7NTg2xMo}OFI)>HIyn_onr`kwfe?E|VO6YJ(Wgeyzq(h%aNCbX6=+7h4O*KPbb3?O1i4 z?9nFb6v#lvk0&0O4VY*}ni&hljV>>u;M;PAy7^B$U4A@#m^Z#bx?uYrX0)}ak z&Uxh@K7sS;-uSPm+iJ!NU6pQlf|o7tJo#vs>(8R)ap3ZZx-{?sO?VO(L_l~EBv4G} znwu3FdC|f+>$U1FDZT?Y5KqiY`4WQWr?jHi?;byV^gmv85{i)!z~2k!!#oA5&vwiRYRInV96Jb#--3HAWjz+*QMW#{JLWQp_9jS&>dYrz4XOA+NcgSFK%d zZ7N1?c6SXHL?l9CO{Nj}LL_%XJ$n|UUQka-}L!^2}(rg$>e=Z46 zzAerww;omUEuj#|^zruOVG8LMJfa^B+|d`6Ce-acw_3?%ySO|hJ!+|~tzFcj*iWQl zw>=GRpEam@FkAvQ2?WH}f3Ydj#Zd_UCf-;Q)>qHP8y3qnjYOWkkuwP+xc5x3h3D&o zR|MCE(&O5hos$cVuPG_Yee}TAG{AJlFQ=Oj_Pfqgxu+=yR!5;_*?!_wU#c_n#ni%a=Ex~5{6*=Tw!DSEVa3Q ziVv?NC}j;_G!rIu|II#9JZZSh$rM&cJ2`$<=U#m&WJs1KooP0WDYYv|kL)c%30PO5f?fr^rr6L&0bC8HrS1F4U)S4 zcY45|D)r=@>M?vh)i)x0-T6*>I!c^C1(1dk6i7X-3X0B>Sa(oz@RDOYZgrb_hGpE- z#rk+Xh4j-~34I(ln7V53leEWv3E3fHVc@GK6qqSeG{ov=sN1muB4e|+42Eb2z=Mejq z2c$^#vbOj`xxtddkcV<$rQH7@fk_ajQzJ8a*h7CA*X*}yS|Qq)d49X^cd>+)&ZJjH z1^CL@78MW!8~5??3J-Q-Mf}45AQ~{CmN1SpUq7FAaL&x>_}KhHIj9Q?{XZqUin%5` z0EVAA|UE{^4b0mk#FWUh`! zv-@tps6<0HM5^UUMSIY0oyK(547oNIbVIXn5Zo=9(%)Ouo*s`X%L4MeT*Cjw{VRabv=P4Rl1 z7QO2?5UgSSogl@);F9V}L3iql^}IKwe<$?y3_b8z)8C`L@OD?!F4E3I3WFEgH*X0t z^=4{%Y9hkJvzOZ(wn<7>)I%_`lj7>|N1T+xs-jkjt_^5sRTTe4lN5?-ha2pQ)pW}3I&d-Gk`K4l0-1S(W%bxamI^y``#rf~*=R#fgPLU!*RLj6O4afbch4L^v7 zsPNGG=Cyan%e;{JNvOB9Yp{d52l;{`*=6uh^ceYWlo{B1CFWeEI2~+gf9zK@E_dB> za-AF7`sqxC4;S`$NH9g+GCU~gr(+VU*Qsm_W_u`ir5EYMQ~PHrn$XP;50)m$6W(iT z9!SmdOVskrYrglrE3+<|IgIav~! zn=@#oIV^7Ib*F1tya=?MV-c=<02Pdg1GTj65Zl>mFeUwB8 zkD6ei2Y#%z!t`ppZ#kJ|3vx?Pghc__-MUq z-mCTMGPYh<+5<7Kj;Nb&|Kk^A%>Uo%G-*6-F}Cgdp{VLcKIsYyH_8`$PLRgs>diOZ zrBE}L_B-rm<70?-WTF~Oj2rYvjYMenD<^QBgGZ-;YFq4;Pcht7%&kU$@6V02N4686 zj_+B*H;%Dcm=PVTqjlD+E`m6;T-&2@Q0JYy&phj=BJHkWCrt8JRuglR4^2LXTexWd zZn@P}D{}r{=AAI47oKH@5^g88@aRzl8RhXkh*a7uR+9Da;r(K(tP^wd##m_PKDME) z?d|ouay}^>_-&kq>yA^;Cg?zQG%0^bz%yx|UvF@gg+%HC z?itIs#dz1VF?i<0eh)!HRb~2w3V17Nz{XkPg#a>X7`=6}%Yhh&!f~8X?pK)DQaF~O zb+z%-88sN}J{%#&Fo$&vI{>xW#Rl&2coJ8MiEg!%R|Z>Q1ksO{d@J;?q4+0zypo?% zLsWtFNM=?YDe|~mlFj|C&mP>wsIZ=ueraAxfFjS4+cjgqEg04M^#ShOG3cn(Ydyl6 z?5#XHY!us%)4LE29Ey7!$aj%_UNvz&{2X0RmL+-Cso~2dtlcOiKaSYJBH_{Gdnbw6 z7L6Z9x*EYSb>O|c6u6Lg&YRx_nlL|(nAaFdCNM*iydR#u1z4gL$fA#aEzjLFWXdvqKl}QN6c0d)i;8K+88RBY!w-YZ z6nCx<-3b(S&p%f~ceQ>kn`69D#rVPs#uNw?EO^EAAdB+kAmR=EYu&BLmoA#Xsqgav zJr6s(Y_8cao6F4ijUpt2 zQUSqi7BPh~W#6Y2I~bz5XBm{50gs=`b_vIkxmdcQ*2bLJ3-v@|8yi|A#}Ax>OjFd| z`ubl{42g#7*tcWLu*yn(wcmPq3~9vSqZoha6biA@Zt0O8_M$Yns*cq;yAE z)e*RH(ft__oH}4g`_z_2pegCndDdS#`z9bCn4$#YqHw=t92O`6<@#gD`zDUk^IQ&0 z!22lTG7S(bW63}G(1>ano4g7h7z5qNN1&@8r%(T<2H;}EXSD~inslUFT;?eS-ESrM z8{3Pv-@56vCKpMt8P3_P)9t(9xjL%XC$*tN&GYsj4uT=De~v+Q|Sq;```=SGfQ@_ zUFi!J`z>Nq@zts>E;PlRpma_3ZPmtmU|Xv6MMp%cx(T)% z6c5}SUCYl7zwZ#6Eisp0P|}9@I*4IXOyy?|HRGzp$r(mNEN_X-n%koB|+`qa1nzRyUg{37HD~YPPhq2f=>5=R8SFkb+Z)0D!9mZIc`l*}C zQr&RurvHVe-B480AGJAN=9K*CfA<8}TKnNt2q?V?+wKCTiwClxL5(UcI#aTvlG#|C z!rnAnFua0SN5Lc?P|>@NxGq=-#32L@`-MQVaxz*wllO`JIoRaIV=u-TV`bPH31D2cxk?@Pcb@LtY4eAcc^%z(au6 z#(6nUC4deK0&+#PemRw$FXcQ5tU-M{cWs2>TNbNuFt zvX*^J@#`*w3xj2cP>zC0bbTqw>Wh;9k@h_$<0ab160K`^-+8*-N^P#`zs3Gsi$$ao zu@ooqzu6k<&?=7g4M8r}8lEV@fquk7hs9+-|H=U|B<{RUPG{$14T!|45Xmv~Y zTT$N7%hS%gzwdVTh1Ct3?3g-#W$UV@*a9x;vBWxf>*0s{nW2dfS$Q&b9@bzVfyJ8D1C_rtFi zzMP*hE$L4D0t=|5>gwdi{|JFiat|+Qx^{uWehvT9Okv+C`lSwQJ8oFJ9D7j0I=eGh zz~zQlNE`8>&a%S@b9;?3j>L3f>~}ENQ1n+GyF+X7H(%?I)QVS3<^##DxKkE$c#leuq~fGmqv7J+|0aeR_{sL+^INJ;TB{um98J zrt!0KMbz!0;a?Wah1dShj_$4Rj6hH9cr)-_J-W_S(c4XYTFG3%k4rMtr~CRI6KLZI z_Hc{3gL@za^kahqrUU9rPGHLWIm^4#@D+^a^0f2WSq9s7dIyqZAe6(wKlWz)?{#Y_ zxlq)1mJ`HHl`ViD)i2$`-G%5Aq@2}NgV^nVK7Do`7_$OQ-W|&gqOFh>=tMYnO7Q{9 z{^;`c;SLB4eL^_!z4~aMx3Otv@9~ewI?)vZ|o znd=$-OH+lZ-*pmv?PO_~G*`Vy)bHFomafOoyO2n&e>rrwFU`ia2gE~3K9l3K!R%pz z*AMQvEhg#sxU3Y+3VrNiMqEWjPJ?Sz2^2;T_%{!5msu+Ecs`&kHNE4KRE`k;vyoez(ummAb^obNQtoTOlCO3qm?&Hq@hl|4OTvBZ& z48Js#7B;ffjCkb%r1EJ&<*?rQFRS7QzU1I@L@)plNf}<&5@W?1b_bBOqiKti0u5_V zeK2eLzksjCG?b`N+mc+QDgl>w^-0fd=T57hAk^imaBCpt%r7>%z&f7K2lRU0Y}+Yp zKwlf?<+<&j702!0UhwRDb)mJe)4ujUtCC4FNXagoW#KW{U`2{k_XBMGA zknS&^TL{)RUeD6S>{f||6 zlYsSQLwBjz=jjw++9eLw=~xxYOK0JwejeYkcd--dF1?!G=`IFK&* z?i=}0RQcx`u@U5oWdxL#)XgEzUS20_W(vFW?wu&&9C**M~eSEV$_qU%11x7vzT0O z*QqlN8rA+OXutrNcTQKo#dZdwLY|@!>k&p6ggiRGTb_7OTw=cBZ^5V%X`-ai)(|8# zU)p#*%yRa&*a-BDnylmMnva{V5*QG|_GZY(AW+&F>*55~qvZ~+zAZ|R@lGW+jAO!+ zW^4D;mB5j}E)F@H&>vd@QE49DGiX}*@$E7FtXvp<|8WUHFG*(j{g;K?OgBP z$#cG5{rS56Tp&&;IYzioe6hK>_?u^U7_hvc~M>Z(v@ zjZ9BQqpvY{fqyZmE=*i0#(Vraw>*z@-ts;22on@e)ZmXYa-jIQ0sG1(Qd8dLV5i*D zKt&tF0>l@_!p$>U=F(eEq>CAUVqXRBaT2fC9UckS&X>QWauR<8Zgb@7|DXc6c>8&H zR??&8R8}lj{T$zxZkG*nhx*m|M)s^KVm1tBKoztGpO@0T7oQ_p9G*3@$%l~_)<^wh zp~FO?4+7gP(|G#L+2sRf2`X5D%RIe5a9^&~DzSHesEzRbEkdunL|m$j3dDNCUE7b@ zvAN~9lZW*ugb4X>X7%0?)dYpQ5kH`MZG{9Np{w(Em$HZZmPoL3);*5|Y85O$7n=?A zHn3mka}Ubb>letMlrxFM$LKTeccn|6=z07pm5EhVWD3vkj?D4D@JF{G`_oVy7i6cH z53WeeRkPJh7iw9M7_Ep~Zu>gDZ=m_)+|HyruCARUbe-u31E>(r#^^C0hC2p%IOAME zPOI#u&{MNK=8o~)A+nZZ%~%Q2R8mtiVi5SO%6&jEvC$eQ6(d4*D8D0w`@Lm5-~;RX zn^5}XZ|-nVAgHt=u^~&4#s{^^EomZ`2k1I>be z)niTu{AgZKBt4g#z=8a_ma}SpGVbZi zor^IvI~-9T6}`{`ad}$h^te$A9A*vfFA2-L9NT>CCSmdM_5%aLFn1{=p!W$pMGAZQ zYT0LL>@vb4CEU52&^24=cuw;jlasC(PSn1xd+Bxm+76|m+u#!va_ zw+DkKMhLm}J~HEfH!8Kq`Sa?dinCO(aL!m`5&f%}3cP)b(5gze42p}3h7Z}00X%)! z!-pRhe@&41#3zGX9-fIU+Y~yGkma*0V7+OUIf;G=Dl%X32@jfvUFGjK4B^MFGJ|YK zDmqrNwgEn`|NLP@JS&vlnJh+Zlbur4nO0w1kvwJ!6umTDACWvA>PV5W*wOuq@>&zk z{i=I_k9d$JHgFT#qit*=RY2lpM%Wz54crY5Fe-MO6~8QX>NE#^jH|cF9bj1LvHB># zC0^B%${O%~ROQus1>*@T8}RNI-Tu%*7n>K@Zp#L_QAP zfP5DfX~fG%+jybp47<0gR0BgKg{;rt8s^SLxPg(FG_vInMW0e_0H+TYA4K&M z27v$)b=UwM^<7e?L`_VRoRWa>3!oY6Fi2#EBAG+>Z~&MT(G~BT@#36xarHdi6^^uP?1dzgl5!4&NR^gbrT14Nx}w5hR#D*%arfnp2cgh%w?Ua$yBJ zz2+09sKQu~{0hdlR!U8@=Rvgi%m&w#VI+&cX}L823q12*FN%r8tKat&d~k8nGH5sr z?fbkST4eksG%34f>*VPz@d%=@P%$9VDe!qmSvBV534m0&k3B}I2IWU}pSmc1zP|pw z>qnm(Y@4w?YJB2sd9#(!AKy;1A+~{@1S%c`rWWxlspG-qYybY8&4T?s5j7n)Kr-}a&xu|_ml(2+)apGo0=VgRXEgZ)( zfZU(UGg<_NG0c;4k8|*F{r$*ah0%8W#QHZ?Vol%PHr>U12s$k+pff&W6Q8m(zQ}I$ zzNB1bSBD`|AkrU|@7>#o%-heNWZTRb0FFraO^Kcq0TI+izIh@AZC6~{;WF$*Q`r~g zrwB@ov|9C-pV96<9G67eQWsLm$wV`4J>|Ue7@c_dn-v(^S`dNSEgeJ(mlu1 zlZ;2B5A@$ak-!z@aM2dpTKZd7q!V0D(JK#esB>nKTnoA{5Jdy=ZX{>bD! zmH33B{(Dx@RBg&y+q!Biv55d#{@CUCB{2kVW`vy~-hFz1Ut~TPjXdOYLSgVIu+=hz zapY6F@0eh|8;L$_xjpFAnX!P(taNp$camQRMflaVKb-CT@HJ(7s5LBa z#$KNogprwaz>y@llmqeHOH)`{NzUUmMb$kO3=EHve+V$&2y=jNP4>!=%6$ zFa1Hnx&Ju-xY?g4_@|2dCT=4@TdgkaxP{((-V8tPQGqP??6QUE=3d(~nMRBq`&KC( zaM`xsBj1phDJ3dj@~I>Ay}G)p zZVD$zSi%Qb@V8H5OmM{$-+YPNDmk0`31Jh?B>ifNgq!OnM%n*040EyvDU91-`KK~6 z1F`-eR%1x3-NXtg4tiTXdP^tXB$B#@ojwyU&UAipKyMeSH)rglA+i!t*KbV?v-Y*i z7r~6rMk2?%zW!R*O=b4h{EBt>>KaC>+J&5c`%Nhg$Z{W%k~24*-LhwEyG!?b0*}a5 zSmUlNKI5ketkkXYzUv*$B&6I&WdAL{d%L}2+?z(3_)03K74OSJJi~G8$Ocs?wa9_@ zbU!O+iY!p5I>*BGP!F-v-rP;+=tlzZ0TT)L{MHsI-@tNa0ba6r!yn_EX}8zY;ZQge zU3k_H1p%Dq2xF~DdKmauPu%!Er1cgdHtEkLLJ?!7S+6t*7dYJw%+r}5VVeR`WDqCy zl!L6-{h`q66IrV1a4qLB8xg|BJ}`v{?;}F9sW+AE$i2C)qP)AoTFW!{IgE7W>cGsv z-6FU85Bk?VWZ+F90pIC8Z2g+%NuJEQ7yUX>MV(wkf9t*AOm>+L0&iB$Hy;lqCpa4L z6rjL2?F=WzD^uadLcndw9X4k(!uprZsp ztRjh;z^?6uoD((vDa(5sGrO+XYt_Sqx6Un&xAN}*$#Wtxw2;|3d8(?TE5vC@tngxM8!ke0PT zD^AMqI>!3_(5%*mGFVf`V%2l6!!FPb9&n%{h38eV+_6k2yQ9N0qRg~sA=A$}S@F$G zib_=uYx0!+CM)f`T1G_et3n>RFp}`-zbmtI%7A=q{p}5N(DPlnQRpml)Fr9IR%=P2 zHP~73dDUZUTvr2stuZLMfbcH@1$l-py!$7Lm+eM%N90J%_*!Ur^H)yI7$Xwcn&u}8 zK0zj?{8rzSh<0o?4UY&NUCFiECgDrS*p;)GceFS1e=b4K%BN2`F(4ZQ!Z+ZYAf-u9 zr=W18yb;@Mq|dheJoS>lV9YkeGcigCooFzd`W&!kP<{ylUx9j2sH+1(**AYryXDTE zTgDy}uTnv`D^ELS$14}0r%EJx0{4U109l9E^vBF8hl_xtRPU8s^)-!4491a7ZTrM)Ff= z>|cGh_775Yqp;S^BtN_W^aiZs$bhB;#$aCf2vNNo&4b*Pj(Xp>p?8nC$#^Qq)T)#7 zhR=3a557h-2Y=$q zgE%8G_2)=dv#bQrXG-{bG^ptBgD=>;Fz<|P6g^aF`?bG3`RTSW{*o$fBa959QWp_= zo+E2=JWUH1$^5F7LY+Zii@c5*jPktM5uw%4z=uVRcVAv$jB=yWGcxF{Q@?24=aN?q z+i!bb4HSCbab=+um zX}})F=wyBj?JJ33OxN6j2NQ~E@^|QTfGAaijZ`4RybP#Zj=CSh1mcp(9OpE_05kISiH>gDWKv@cXiI2*td8Mz=kk zx4roS*P@;$r};T?-Y8SaS>>uvj-sZ54{xyWVXuP^G5pdRUI~I29~A-Q#Rk|5(K#L} z*cSGSGX8@rfGJiM8Tw>uEc@D>ipa*&n&-8NG#BNa^RG%#(aEN**j&S)`=IR}7y1kI z^~tL7_3@hH7GDl5d3zg^e=%>6z-L-?KXxwo_VBrHO#UIgGB;8%>R+h7rcS35Y3EF?> zla3(*>9ti-t6B5co0gQ641EHh5K8AVM_Pcj}z z1Lb(;Y&R^AMd6=vBLYXb&wcYpULouj0#7=sKRC>sn&|nv`NVOlw+}5daL^KEy-Y=c zPE#)yWWI!A(R$(M;Znb|?Pm=7Zxm5lv9_c0V3wQ+&SMx>Pi|H5A?-v2E}3s!%P;!vg4J0#)EHSWFMmCR6<;%Be5nd zcT%0evxiYdp=Ur^Mh1TZ$eL%5EchV+*~S1Qh;57HmWKf|p8ZXevussI zB>w8lv*Lljzf5c&hGbyY+x#kdJLFmFZ22ce6*n?c)>b4}B5=7<+oA zV#zepDoxgoxkKP%wlf(O!GzRw-NLFxv%g57r%&tkTn=ery(8$6$FfSypP_MQK3g>$ za-&)53UeVt5-{%CJ;}9~%rWs&g}$x7eGbkLUr=&-NAvn0>#p(C%&&qMJ0fch&MNS` z)m{zJ=Q%R}rUu%4x#2rS_Rp99jwp^T30@C{Aw zP!_saU;P`Y;Nutvhrn%EnL%m6SljK_o6tf?{@nMr{i~RQND5pn4UNiYlb$PFoG@VT ziY*ynWL}~GV7L#8=M!w+YS;pPkz@J{61YIZfBuj2YJ22W`V7;M${}ye@zFI_uwT8d z_&TqEmK>_)fC@nK$rrflr{k2;aBvZJR6)Si`>VK7xw>s)hPa@zb%r4vQPdUng~2%? zq(q}Jqh)66I(p?tD}i^yn7o3#0*xnN%4)u5NQ#sL#RAXCSZax!gagyj^&!hB>qqX0-jTs*{jDRvQp{?B1 zfNIw7&PZ?gsxk~0hR3k=r|osR&Svj7k?=p_HKM*VveTNBdkb_LiuyN`8Y zB?%xJ5N5|?=Q||S{{AiSDJ`!74}R4Yg0abrsuHtyb7H6r`691S=UBE*0b#9DP`X`^ z2JcgSll!W@!&?E~ry057cueB6KQKI-yFl*%>05@DZ@i4E3*?JrZoSka{b2CHDa;4@ zJ`oQ$>jEEseQLJ(fhasU74b#6jwtN$x2abPRy$&#A%~kNH?7*3z7ie1K0X=uhHpqH zQD#rdm(6iXFh(!6;*O8Wm*(-B!m}DZg8}6?=uqS^L>$wD4_8YE!%+nmF_D$}X~&x& zRxT*qTg`fxJ&poqwh_BHtgl&XY+zszj#{*wNwVz`GW!w*EG)}!)Cwd46gbYmS19@& z_v~L3X6QWcB4>DEMWhhk_ybO`;T#rLed@~6=A-%IAsyUH@FB8lmH;<<)HpMh%@0{v zUoHfK(`7<)qIqS+Hi+R8HN615d zZUfU<_sComu5(wX^Ip?S$5;@hXMEL`#W8rWA^INIrP@eF(fN zcU1D{jrQR;8ArdO`@P&eI^_4VtXy`2;s{sYpv&7}6ac%X(b6=s_SbE^e zy(U2wb4N8cm`5#bU}F$;W9diqDj9WaK#Q1Tx5=Zx$Ku?0NpW_>l{_}Q{(A>t7DKw? zr&Q$ws_NY$qx>|C)n(e1vf@vwp^6q(=%O@boU(J1G+EX`n7 z7B+}NZcgYSyDESr4on+3!UrIka0u)2;#Bx#@Ok3sL=Y+977KJSPxG;sl2Y53i+`+A z7!&wtc;DydD$$3f=&e#M6e{kpW3is{vc(qUX!}i4(h+vM9sOh|wMBg=XiNl?ICiP4Ev&!UoEcb-rwG&& zG8v9sDvNjQ)=vq80lvR#_xHXD@`C3EJW$O%UE~SRof_fGfeiM9Eu-AYadF}lg+r6> z99k+17Xuyl@5k{lnN*PCyh@b&=0cbaMaDeDP57k|1v{TbCAY z3itgyn}2C5JQ>{`P|FyZaHTt-OZ%_|?kn-xdh`+MoxuDS&=b#&PU6uoe&WgI_;&Md=1p_Kp0cuBA%nk>${_u!DNmHr&uweO;lwGxbDCGGUD87;MvUQ|aS`bZr=Zd# z0S>LwC5v>`+V8HL>%p(ytuB+BD65)<0qM^EE8M7=_%s@EJ!;K`GeNNOJ*bqF-)IJd zkYbg@z|9v+slYN8ALo;ILW7SmTkgh#%gJpQl5byr{{f@oc3VwZ{WKrGb?481uP*}% zVqD?j;bA~t_-$^2cnY#j4O&-3 z%{==Uq&%i50`;4M1tEDak+5HaorY3WyP}{7CNAggtOg7`q6Ay-s=?my0$5;)`vS0T z^9ytj@XfdDksQuvwo^3TN+}NLbCAyWIF1>%`UH5xiHP*cvL=Lkk$i16iBR+ztt0Gd zA$PdWocKF)@%>svuWFF=V3vfB_darQHF@Xnx zek4BKwe5K4w4w&L@0WbqtW*8`dn|WSMBb)suN2X5QesFtElj%PS?bXd6Z2V=Jp(@r z(v3V)-|y`ibRbnmP5pN&!N~2FXiR@Uy*5JBZVpP_f356u7YE*$w_-jQCTq~J(q<+w z@JwReOb6RoA&fzK6sLkvxR5bE`T-bSirw()TJ!GEZ;qiaX;lTY9!QpZ{yLkcoKfQ8 zqCcHi_rkut$5Bl8SLNBj!Cry#T)8;P-mG(>~V% z|JRXVO|ft+0gv%5ekzJOY8l0Q3}AMhUk#F|{hjP6@DW9`%CWjU_A0|jTim&{TiBO% zMKe{(On9TwC(9%a$E5%~&+QU1Fms>uuTi^T!?xPhdTeY;BXY4_JNI zSC@BCJ}PAyOTjwj8*3+-5$vFICS6T%NxmjIBayE!@t-qh8xbkpCscqOKsJ89C9c$f zg8oQ!ykonqdtqHBk7={+eEssnz%FO7f~s-K3%@Ws=mz(wm3)ys86fyQfE-e{{@u{3 z)4i(CboF@T>sNbd)O!2DDg7Cgb}1M0(_}Wd>V`AHcsQe15>VYWF*C_2K6@*(au%ud zCsTLe#w&XO$azuu2?|1Y)Vm)SYqspsQ&IB*-z)%ZdFlrR@Gmw_J-XVQm|e+hYSk&W zqP(0{?Yde+y#)#Nsea9^hUVK>P(j_TzZuuuqXKL<&Ph0Eaqw{3289ZgcUVNOS3Bgm zvq=Z6>$~-BUbn-DAKj57oz|>?*dz`KYx~zyP9>V~-;6JGWwE_5*Tis=G`BQtv1=X+gx?n|#uY}$1V zugP?-TbP)aOa|}u{}B^_!G{d;I-wxTtK)Gfqs+=Jj|f3Ny0rAEKVpu5RC>J>A@)|f zXP4|Jz=;VudG~(r8v2%CQ2Yqqsl$B#smSZ}oTpt3<94z(rt4osJntAzZzLZ-b{r=o zj}XGUO!vI~5QzPvorF8*;qzUKm!cSwh}t*4AK(~5I;32SS34$^#qJM=G}A_}h4=F% zL240FGIvHHNm+FYg;juM3lM(k-=BH_d?FgaV1M-z+y%x72sv5E9ftCL!0{2UA7K!` z?6|D$?=inE=QfSM478LLpSW!#PKA^_H_}=m_`2;Qh?t)D(sGl7Ew546>5yi5E6~P# zbJ5{yj18lF%gdbtIA&F*d13c(vIlXvpQ;ZGAV49Nv-`ityPnWc1l;=jScC{G;s3ng zRJeH`><(I9=!hsM0p{FStP+zR>+;cTWOa~Cl%h`0s2CthG(cfoK|kWlN5BY;=)54p ziF-~&#K}AE1nYXgw8iBU!SrTUM)u<58T*GOrC5GP^e>mFi;cl8s~d|J<&p(+)NWKSJPh|S!_`x=`-VOZ~`GC3L;Z<*O*g1BI;{-VdE)#HLOev z_f%d~^r->{HxNDf8+sb-jd>(<*E7l@f~mW07b^R9A~!lW5nTH8-Erj0_fIE!2juX$ z0AhlZ3~-@3jPxYgNW?GVdw6qE6E9!kQ>L|hD#YbcGL@>;e1#mO)jmC!LL~3_W7M^% zN04@1J=kU>WPjz*lZB)<%`L%H=GC1sOr8B*;PtTp^KG~AoJW=l48z(6b-=nC|J$!a zadr}*rmmiyL$K?OnG7j>e4ri8DS$2W;kYONLBE8=OG%unX}rh|qh;ErVD6@M;0a^y zj^|%%>;`$f*eG_pv%Xy9eBrrH@G9=-$mM^-NVaP$bMs$$@6eG4>%>lXU2*9)H;l4` z#%jGV?d0Pw33hky&&@>v6cqUK!2KEm|LCbAq?HABR3j+8(!j!2bvfjRb6c5`di0q5 zKMmj26FKy65FM~qX19;aVjdhO2u@1!hgzCT^S`)i`**Sq^geZZ?-p=p~ zatL|nlljKq=3Lngf0$|=Z0?ShyMm}(86G^xY2l=FEcrux`l)2}#E2GU|I?WUaCw&7 z(Yym`LwJv=uCQ!!?KnE!;x0yZF6^cO|0#4N=g(k&%ID7?9*ki;2kxBq8WAl@rPU)0 zod7KXOqfF!1Ry0TvyOX8aH~wdENjSuG@?&G@#=2`3C6!R{IL<#=j)AV-iXZY5DfZL z0GFQ;dSD+drObC1=DQ0#?pL?N={9!ygreb9Xqa-=2(;{S+Cxu{rZNK`E!xgv7z#h zbkd4$(Tf?KuXavorkfq}%`0qHt**s-a4b<*1H$hV~hw2+4hFAu8U}4R)ON2x!mcXk0a) z+3$VpCw=OlIw+X?9i$V&&^K?`zdIeg+E|_GaxBACYLT=*5^`Br7$ui!gv8U|h*`g8 zU}cGgn}%!VPK(a^aMHtApD|HX;0fIybBrP7)ToF$LT{O1?YvX4gB-1)i=SfgrSY>= zN_a+*>+QaI9O#L^aYz;`!S=C8uT#Nc=l@%Flpq%u*X+3QL^p~RasQyiH}n@yxy4>x z1e)<$|4wpcpsaxAOVBEJP6BsfD!wwn;ipRBvfw<^s`+LTreh~B5lxBuFwpR+%d|wq znhS`MS^mXhGp!ONzTqTH38R3?0bxPlpD(4!nElJR)JWXv->%k4JA&aSPLG*Z&c3#_ zDN#g{&2(W<68}5nPiXSsMeVC~fsY0nLTOm+flalstiu*a$bPNQpuSReC(QZ~4ExEN z(1!kxV|`Q0k0BXXgjE+PVWvVsB5d!A4iL~u^@3p-q(!z2qBGM?4n$w=6Qpa$h`@_m z+3QzRSiWV5{rVDoj@tlS3Zo~T7a(hrIKb5oZ&YZIh0x9CT1=)@qzw+B0+5HgkYJ>* zLuCS9M&4`=KfvX(VK@?z{!nV~fik8-_Uh6OXV%t!>%pqeB)4G$YJ zkosh0{RFCGrzga#PV31GReL=SNui^`8@S}DSrB9ni!XdUDRY}8-Rp#x{q*9UfWMz# z^;pVM0K>!poYsJumkmT1eHuvLT-VF{#cD=!&snD>_2G?I+EffJHp<2kq$}(m3a+)( zdK%8GE5B)j-e8UfIsA9V1pH$@7JPiaAF}qqZabQvRt@XoE_M3vUAtqJJ5Uf=e<0`$ z1OZe7W~k~WAz(ew^YIDquVy)?_~s?#!tk5MA^Od~TgDP&itEeo+?;@Tso=;9+|1P} z=-r-NDZK7l?FV!eBPg)85$qiV8W|r?zg?E40!gN(r0ACShDNUmgH%FNnqmsrnkiEG zcAJ|`G9)4>LH9kq3T`$Afv!5)#bTk8x-s`na}Sjl<|c);SOj_<8T#V|2JZ>DI-PQZ zo{1=vYHKHrMBnBvHp`mrAM1Yvd-pFcRDNgKk|tVfLl+)TOBDMWPp<@vfB(|E5>Vv< z;sx?xX;rIOiAb@N`^oFfbi22Vsf)sE-Kg)fo$~L0N?&|sDFeJeL6fG)-AsnL8Hl3~ zZl87t2{FUUC9pdgS!z*M>j3el5+GTanJV&A{qF{GEl6)a)URUVyC18N+#n5G$0Ds` zdH-(7v1)Cc*e(?R$N)!R#6U|VwW%DgQECM;Gq;%LE>tCY9D`!fg06gTeL|Sd_xN~R zCME2$^#AV#1(&c*V&md63b6i`A11O@3KNJ&YF|Bb*^)rgX_tDJac~i99=s^F*n%t z>qW-vZB&h?z%{THbMHYHz|5<;astEW-m_-HgYU@^__wn z%w~%qutp~U>DmsjE9Dm1KcDp!TyVPV`Y84y)bZt|vyRHk;7;rA26px)yZq+iY_=`U zv|Fd1)fpCPY1MVKJ}JvjN~oqBD!*J}X_(3?Q9mm$8}^``I8B|Kk55PlbmL0cHQiY1 z4@r}vYd96-7#d@JA6|UjhsZ>Nda$pgBDKCq{rg>PihB_W4lLI*D1c&lDawkfRPTq_$FcHp zf_f%3f^LE{+4Fk`FTG)#jdJ96AZ8F!efr_}QqLDKTwz!Mkq9_>&Tn027J>zL7A|?b zi~dk$35}B2>2!#joGC9kh@*1~cjq1H=rvdyTE046L%lO$0vTAEZ#{*L1u40gaZ}&~ z;NKE~2iK=(U*BLjHGUWoZG7t|#bpiLi9c}9+C=Vn6}@hG7x&tKg@!IqDaApg<4y64_3=wx-@5 zU1>wLNPKUm@4?3Sl^Zu-df=P-6c-mVf;`Enr^ZUE3Ok zBGm!(^moRNO~GSD(688w!K zpME>nMgLmB`cnE;-@sQ55@UV=qy?utXc(!t$4CNX^ZpxGI124a1`b0~ugFSp&OT^B zYiep}^E9=|ERM+n1R!OH1Yh~>c8%-7>JTe|;{_^48Op2y%R9r~u=8m<5j7Iy&Knda zx7ED$azrwZ+#zVbXXk8pjVe~<=J7D%J&j79dwVcV`Ji;nb+TCZyn6Kf$qnCZw-~vy zlEp>!=%e~B0iMJ&R))gk`5!_4ox{TeMo<2$bgQ?BfpCmOh18p0?^08ZcqN4Ey#Ys7 zcO$*ng!^d@S&QRMx9u0}|B`%16PD`-GJA9tQh|rmxeP1VjpM$p9FT`ubof|X`{>40 zj_ruj4!XzL1HTcz_;qTNPNL3@4OybW$DkN zdTu*~oC(NTXEy0}d%xB3{8ZAqoc^J2bO^8*o2Yp(vNxCy(jZ07bJXZ<4k1;`tlO9R zP9G$*Y0^|`)DfTBdIZJY5P@S>&LRNA+k!!kWHsHW@v}SH+S(=dKIwqnk}M?WOJw44O7o>PS}DE_M>XOvC{ij8WOC|QFicfR`zpIDN@ zg_Ac6N5@)HffnN7J#X{$QrxpD(uAn+7QUy-J0az6+$dWHrI?yHhy2EUss{ za9>vn>|W1pq3dXT=Du1Tk!2jS$0|M51U$~G+CG?w7{aqSJ_2mqMURMq-|Fh}Lj(Vz zz>yO4eY)+L_kBc0kWo;)L7J*C6-}XZpTWyV{^@f*(qvBu@MZ9VgJ4t(3bqi86f^*b zut*YxrQ1ag`aaRr`IcGB_SGRG!~U7K9M}3Erx)=6vJX^C)g;Q|_)F(x!xLQB&I&%> z&>1AYf4{x~8|d91Y1X{#OCpWYkDvT19h(@vuj$}&^SwdVO1*?NYF$Ja(;WvG*6>Q; zr8HWvBD)Pq=`e2FeDU`)YL9O3=F7fK|)8fa^A%4gLExG6k@rSt~aOEZ${sYKMz$Fi2jNp&YB^qx!mJMAhVT;{TFdGQI-Af3*>Kon?VC`JALeuOt^DUF$z`T8w|V(y(NQ z7Jq$sdc@p!F#n81oyJHn>fd~jY3t)HMGXwv#k!~Vq(9~6JXf4&oY1RFLxoF^8$8XX z>ir|NfwnWV<|adO!hHis%6){xs^4ogb>4);Azt$WQ*>W~6idiLaRz`8B}gomw9uSh z+0wO^CP%?mkt`eEtyt{OnTT|0WE0mTAtCa zJ)~(f;nY@8&FSGKAP|asBJ~<9!<^{b*&4w#!n0lpk?5wxtCe7JHr&7E;Rm_$2B?_g zZ@WE9dgK=ixz6Fs52gO)qaYy#K_ihJN~R5wzr8kY8MKjLe#)LxQv?2>6%HPsI;~T+ zV=Z|lEymrQu02|fNh%zMyM4y*?);|zfpPuar>uD0QZ91K)2dq1w&Td!+WH$YMO5}hm{w5W|ZoCL-`RAxUMh9t5whT{jw8snKTFAwVEMI zb`IlBm&X}t9dErluWx5aQk0=eP1NSrey?DSNVD~NO^Gq&ei|Z?nmgGqPSg0fWgiHT zeEhVR91Z3Xdh&yI$UK#2mdtf)c%UMuI%5>c zSk`;m1BT0R)U~;px$n=tiWW7rACk^Ek%U4#OP~0a!l(F39<=PQdaGwY^NpBhRsRy* zRdH*n7DX{{Ox+b!IYB?s-ma&iD|<8I`R0V5phy#)FVoU7Z(#g$_W#FI2K3(u%vib z4Jku09f0!Ws;^8*6V);#Y_lzcU@ITXG)8~u7 z{u79}E6;?usIc-w#S9@8cMD7)E35nJVYh8fSoBCS1*0&_*h_!_H}{jz1@8o2RNjqm zRBA`CG-h!l4mMeujWbQ;*$jL;S%$eJze+)nVcYpRJhonKQ}ZZxjJ4y zp#JX6)fE4*7N=>P|L@DGc~LFy4_lHSBVXc#X#sv|hKWJM zX1F#thsTPZ?lSSKj(y868E(5PtfYh@e*Z~*?GG%m&voK_o0Kr*T%%2#_7_wyBvpmv zu@?j5%a+1m&E9PT)naW;1RRzQU;Rg{lailh<_LhEd4K>Apxh3}EJcXd>B}-oIj4_W z(h{NXgNF2#a)38N=_X02XQ6eA`o+SgUHzn%>_2hR?r$s$VZi z`*m9DJVkVuXQu|nZvEj{?qpjcYLj>{M5s9?h-Axo8nH{pn@6m7GeV3o4}d>p%-a47 z%!;Em^QoE%9?Cbhq4y|+pMNKvJ$t&Zbid{y7!d1pH#obb@>Q>N292KB^)|I=?r71Ca9IX5GGAQ@b||HD9weJAvaSv)URFJ z#ZjQh_Cl6t=EkGSVRx65YkoEO&=U7{x)vHpeXP^d8Tq>oN;vFksJ~=y}tU4 z9esFw#8=cKrQY&Qpccs1h zp#pSBt9iJ}vzAdlMIkD*w5vursF&0=|xw$|= zE*FZcfiQ6fO#KU`K7DGWFi6LZ20&PEvPuwU0CeJkhk342t$#kdp=R1^+OBi` zgA$Y1?roYjV_pr6HAYpQC)HK2OwLJHD^XLZ&cifoKZQ1K%W{>^8p38kBxVq;d8 zTmFV)vgCg)r<o*L?is9n?H$wf&>FcJrIl;A)t8fjLMt`droR4#0D)gNnTX!_Zc6EoJ!N~6qA`>uh# zDl-Tni4jzCQE_>66x4%_|xW!@2Ga9xIW8H-MV~Bzh5!k zvWIJquP&y!1p#>fL;yPlE27gyE1%|rinwE(a%ZppTG%-J#G(~HJ54)~z*#N1RI_WE zsl{sixv+p)%9Ki3RF`+0bIcJvBOUjtD=WsLHEwZ9#cMbMj+aGaDuVsFa5W(?#z0Pz zD1D^h{>qwnM=qwBj2QJ!`IZyFjgIumX$|MsWsvug(`Uu@^Zd1i6hmDmEra>O@67kT zioX8>qx|!RZMVWfYeWB)5@k%Tu*Ib|?#hqN_a94F_zPl->%-DNRXTl0?%AJsk{a;Z z^k3FzqXkM~Uq=CK3@-PUxKLX%sI=&5ZqTKEnFYCMdQi;WO61b;k3fdgviQ4l<2zi$ z1gjQ?e^LcCqeer-h4d&eQJg{2U9~okSFU(}@N@jqtR1s$kgMu#rvj||-hk%Bb@xiH zKgkl8Ry^_DjWM%3>yNPo4^!BqCQ>5EKT1{X6H0X`QRYwqX*TeCz_>`#Z}k3}X9u`_ zh)F?ZSvOxhe(xt%Z>|V)Hz7tp^ET2p6lPlRh_Law*SB!5O*MvaN^2BA*hsK0pD7Ew z`Ns4HNdkUb5RAXcF~B?rua3Ls3KHFC+=}h{ElQhnRF_YC4J^oGvmdp}{E1)f^T?Jx8vvvGSe&@n-wMI%q@}|8(N@oo_K=c{pMWE^a_dS`gZBA6Yk=#y1XR zd4@j#Fb9r^-HyPVPJ+Q80}u;v*Pm9^9Z&=K1g3fAkTNo$Ox*m^ecx4jgn?(Qpa76a zG>G5d&tL$KLM9W2HJYM37k#2F+h@WFE(;&IlJ3oHK|s@t2)1sY7`9|kh=pAs^1+r2 zFefDTmJ6jA@eGbND4p7-0W-#cs&o-Etb9{t7r1RY5m z;h2_>dyFO^h&-QrLAC5ND6duIXf0Q{${%6l6HVAGmXUG2{sB7QjD=S?p2Iw{p2?te z{1n3PczZITHYd*;<=UcT@SGH}1P|`d3QVX(c-NnA;}LTwMroqzU{G>^u1;{tFv-oP zI#7nIvJA0oV!VGr-+BlDX^IfA2T@3>o&3Z{T8HaXiPBF-Q%sH>$p(h9=)0bUPMwYO z#^&5ashZQ*Nq+8_gQ}{!6SXNK=*PT!`m_IYb=u#MdT%SY>{)>u%JPy))~pS);o$=D zMrwb+!~&xBQ8^dheJr3RDJfR+cC+GwNEz|pILc4dK8G~#vtqC)k+SG zqQnf23v?t;;{(^xBtz(uLy-{4A%<=&J<^YYp#u?h+7^3_Sr@I*_e2~YrYG6NuQJET zX85j>l7ly^%I31#B@i%{Mb*|VinOGf51_SMvoqFyNAZJcoDx{Y^Rdd97 z$sSe*U>jcXNAbE8odvb|o&P(g>OE_I;67b;e;Iu{1aS@cyV0o{*~1P{W61J22Ggu} z?MqzY%;gz@Xkp!lBVi55i4oRH?KMq$faerPfbuwi4fND|5D#!5VSxxP%PCO<>ca{J zs!CU0!r+^m+aI$&`-F2 z;qHq`6EeAyeVmZuY7D=INSh95E-`O<1|}eebckWHvsHMC@(!D$H8;7u77B@XU(A%q zcu$}EK^>Y6R?h_pzw170a<#rZCO+-9>Y!0&2#D{nuFKYec9+4=IQTmy=Kt#`t49{gn~5C^6-$x^q90s!eS zE!Dm^nAr4&xBqv@yW=-O+udCrzbeQRjzhj}_8=Q`^ZSt1e@{*m){bKytQlN8Z(V_# z5|g^(H|gtZ(Jp~np9|#6#a5cX*R{d0fjnb2nbTw?H$A9v4*|%_Ocjat==xue`31@) zO4?{AZD}-oUvdoyb$v6iXtr`sa@)S^o9H>WBY9wEJn(_+;^nq1zVu&P{(btc#A7WJ za=@CDBfDRz=7EAc=fNX)&rd8^gr;3>?Gs|qiQZav+P5GSBrnJWY18Fj@b}KNq+(N5 zDk(sOo!wXSkHGY&kgslE?GRIbWf>EEWbV%f4V^suWKNGK&iC895Neus!(>M8+N5%X z&4(>#t%_3z5@dll(fV{Qb1GDnbLG;Y#QRHHiDe*EAZ5$nheD|_= zX_Z`;c2iyOefJn}i3Bt=ZA~M^h4>r!Htef&-5<6hqy4}H9L6i&{Bj}8$nSN`*U>U<~+?G6clwgZ=RsSa?N3A zMGcZSJ}MYL!xX52VZODD&fy1k$9UIGAo(&$iBQ~0_{x6rvu0hHRVJ}hsWU>2j6rzm5^m!Rn)6eL zd6~YU4-?abu<`NfyP$yvb+Qy4?NE&m%Qy6C-nhk}%F25p%B$_tI%)h7xs8%vc~P#y z`!AUL{K|Pg51+(25`w~pV0D+s8Mdbv>v89Te`tD#jrv~CgcI|BoDx^C$b3SMD!L`% z=9`Vp&8lngX0i^dRG2Hb|3c$Y{T?6NkEQQA4R)Ln3i-mX;F<6HkZ!ZG(Pjii*G5}v zH52T)4M)1lXw_2KtKa5JwdL6h!z6;3w0~q+q`ds#mzT`=U&_r!xr{(u%!j-K>^<;& zQRj$ZXdTedtl#r7Z~uoMzy4#c^iKzV;HmEgYBeJ7+wvGeIs|}6j zdtny!(Ur0}a_}*pno@aF2E+3v7pO4P0nfPjw7AiUe5F#Zm{hph%Q5xfw!DELe*C1j zGv>%!*09}iGE+dYODX4L-O5+md*Rbeb_Bjt&1GI3@#inOwev{5X%oZn3P&Co?v9<^ zL^^>eKYZNhn#^GpzzlU{m=`?O{199WXj1^E-gir0-XtBC!*c%#oYxQ1w-d$2av;1@ z-+nX#m``spo5R|Ww@MTA0PaJDeHz1?$KZOY$Oim&SrePDPd5J7pS)8QAOVd9Ud+#% zP+FA#dBmUa7w;(ti&Jpy(W&{oghqG`eEwBUvJgO*zP}vk3-yk(OX&sH53cf1Snkky z=prgiIIR+6b0f4ZdBlU5)J}u$TSSCLZ*&vSJNZ}`H$S>DM0=c~x9kXw8#CeP*cV^Z z|8@FZC+ovwr!rH8E3a6y3Xvs88S|NCO}zdTaT?Y0o3*Y9#POirPGYMn_SVHTo~X*{ zB9#d0BNm3MjDHJW?e{|DiCK-No)3@@<-_6Wnb}WwklIzdKwDyu9EH>n@%K*&-_@<7 zpVbNDlvO`IL$Q9|oPzf5cT>5iIh)EDb~5vhkcx`kX$OiSbOrR@3lx~m=VK_kYiIMJ z!hwoM2JaKk6u`uAPja>MmQCK5X!FC|=S06dp`yG$WupQfk})RzL;b!Z(w&g>_#2Av zcK@oMrg9;m53+d>KVp9-AdK&@;%h;I-#bzcGv_17^cIMw`T%xw9P8|aftN0G8jN_u z(b(rqgzMVG&#*eX+F(=vv{t{=_KkO^-0Mx7Mz1JcEDuS|s%m-6^t#V4bg z=$O}Z?4SEWpLr@AxrdF=h{ctK(_v5PcpotUJH+E5R(u3d*-W_yomWE_ zCXa*G6M4xj%|rw-a)v$D8fNkjTq=oaZ!;o3*d1pD>HDteLAgNbNsYVQ_X)aMVrI>{ zF5Ja+vw{elBv|49_-(f^QYkuH%Xa1=yI@~G(Fd6h5F#k7&p%DSzD0F@X=j^X2nQ;E zoN%v!cNCVb$3O*T9jjfk9>@bb>I0=?>K3Ng$?of4v`oU9=vmC#2F`QeW-Vwn)Yh^e zM40|Ipk=$Ue^yVSyLb}FO%FAp>NeJBPhe9{M7XkglXodKK>SKh8yl-ykH~;F7kY=T z7q4Fn#mBBM>TfCS&VJDLux^Lz9$h!G9nqk###RPl`VbM9uiqGACAR}b$ai@is(G=X z86}92%V&s2?lZ2%AjIGZauC#pY#eTLZTq4g7XFS7*B}(hO~XC`B~o7?`SUL{xYO}` z2e3IlD@@6?wsigKTA>u%B~wskVJeK_BS@PZ+BR8dJqEQ~Hyoy<4*?HB)KG70TjkRW zb&VoRUOhdK*f8_>i81IBB!uDmC}~lBnGfry2}w_S!K$Rj-Q9QkntsPNBF&|WQ*{bM zZbccjB{|KjIFo*kERYCD2?hp^YieX63yMt8G1;M1COXD2o7pU{{KudF(TM)Di$ur< zN5cfqEvlyZ(j&@y$s zVSwgMs#EdKb4e-MwSTp(O_BOA_ zJ)i;dHDxf!aFYTgIqB%Mfmk`VO$zGMlbe1x6=!?G%M6cL5=%8;YIgCx`XT(ge$uP` zR7NQs1j97&-S{I@$gcU#0qh@3A}&#vK$|fD{+;hT$aH~Es~cY|xFpYcha zHODcwc~QP?_&T5T+0dhHhx;oYkO4c)aU%&IU8oODrqQ7P~p&e0}eW zu4f{ZZM4F#59w3!S6pOV(mF3AU%^8i1N+dJtge&>`YH6(rSoC z-xeb6Ax1tiJ!Ff*2aM`B4kGsE_m{7lgjsIP3%FM4U5jYjMH8pFziCXsLfPacqf=QB zty#y<)1e@}L(@QkNF9(MRcXh{U$f~G$gQy{8e>O)v}qDWAUnZu3u^8E{$W#7INjGbt*WY|jRkkbcpSju<_4b* ziFkUzh0eT$F+s(*_uA35fF2FG6;jL#_5fMJeX-SK{~HW!2-M09vVZ%d;i20$B)Pv6 zPLIfyg{MQ-@(pC7+WmgWmM|H%{Rx7XWdMj;`BXqaK;ToJC7=xtKj4l^-i5_m_7p-_ z&W06b#NSkq50aQJv-cfb75Uqts>qjb2ik$%bh!h#NO@Li!?7&voX-a^w%mE&?wD=f zeHNG4I`7-ou5r6kkzborR0Q&Jp4a>QEXVOQjQ*7)@!2i13BU}kA7h!E*JYOo&gbZ+ zV~Kp%Q#WM2_+MO{SPm&V3>PGiWCt`MFxknKh8F%r4Y5P21xYS8_Uliu(y3s7IS(lI z7a|!me!!^$Ftccs)Hw(rv<=BSJ};D+A7#qxT_6i5esB3z zIDh+nHTl1|@A@QG<9uN0N#E%NoWz0+y#SEkUxT!J=v_R8EhK+wNUH)V4u584dO@;=N#ohG2 zoSW%#$mhBMDG#R?0s(65;C$dn5rqRt==cz4VW?cqO`f=WU{7oq%YGh_j8+~c$?c{+ zlY^3Az&SHe8qxUy?f9}*2eto5wN?j_we2rTyL|&f1d|Bb^>@ps8x~jOdlFiOcrl|1 zbRNs;0()~|$+3|+U<7>?E6U5W#>~IIaix(l??G>8Xc|TaCBO7@Hza-Y-Mp%_RXyR- z%wht+2>|Q}+yHjHqKvb0-deu~m`KzLQ6Yw&6W0X+K=2(37}I>fF%^`4%AG_8TRPii z%b_-T_xsMP=XF9*0KYpP@9+ZBzrXFLO)}lUfF>hKM9--?N{zV@Rs5etA6#v1J#wMc zdZ9nW{>X_njT|aO%7dbA_4V{DR7DOoRJOB`+DBHM>l^Gxi$R`fVRJ5Ejh~T$xigB%fL888+p3%;$ zV*-n%JSOH(Z@)PYj|C7kpN}6YV0LLvxC3I{h-~L7!YYjYFGW`#rnzr5XHNDvclb>R z&Py3O;FeMlJ2D8vx3GJg#)Ofm#U zUXl?Jd`=^jN~bn`x%N}DNX)Xg0O@fiN9jLargR>SK6vGi{+o9p_egNOr>sr@{Z%fs z_9=@hg$W38svQL+%z2%T`19Lg~rmr-!xTf{3 z)Rg6p*v-b{obAdhKr7o(80Hg0iDK8bKX>O#X5!9{JUayQ_D3n17%L}aXAjK}OYB3z z)>=B4ldi>;I{o>?(Q-nEdT#FPcAzN4_6e$L=uU0^b;elK`kYPcZD`(9XoVmIKpsBY zXyEsN&3dnq%cxzo7RQZ_i>cq1C@m@JDqFS*RoNVozkoO)DBvp>!BP7NjDj-;)n?Zw zti7m#bLtS2j?do87jv|QC*wQ);qh+C=(8R^?Nf-gc|&k(9`6(pjEyXfivv^N1NDP# zvw8;xN;H;vf%Jm@#*)0v_iygsWJtXH2#N?Pn^SClxkyC${_{0$$y3l}q?g9V6YK?- zk%+&lw&!G#W$OlJ%XciHY~9q+dcbD}1WHu#OxkW>w}y@AEE^D2>NpNC$vYQ{l&zT zRw163U~iI$U*=uA%mDDJx>Zhc-CO=5?Ng$8&AX3B{ONpOx84WlV-f%{>h!jL!<=Mx zYX8_?xy{@1Uj4(H>229LV{Q!zJi6!*%7IPd3Q`SFq^H|!(d^%yp&4TRnvMMP*uH_M z;je`~>$A>zwAU-D(0zPJjH`?84Xx-33U)*5V8TE3&rHXaO}jZvtQ^-RFrKt*H*YZv zXbZl71?2b-JjOPIdlX#)*xm6&?UcnRV4t!l875iPxAgiPPBW0Tm;p;w_dE>*@qIq2^yDS9;poWI#z60OtIYjo<=-h= z)pzWwy#t`ZLah(C=Efuj-;d)4=vUx0tfka3KZ4i_|b&mx&2|1dIH)zX%ljy+H z;};)|chw*UQ)qdz_V(ReupO+zFGKq_XV&R;_r4iP(z}@!sZJ6I9tnTb`0V&&`x)wx z0tTdbd#n=7mc0|8cksU~=JE9DD;i#Kmc1?~A&2dn#J#GC@A2TBF?W7-DwrfX8}JWlBEIn7eHzV4{E2|#crBlXoWo#f`7_w}y!C`0Z?$u2`dRT)%KqXj zY0hPTXCxNXB95q)))hxyi`b%g79!eX*}Izks*cAOs-51Lk~5Jf&tyVkXUT6BdELaK z-LFMn0u;6*^ z7OEzdf>&D3SPC&aU-O0SNC z^s|YqlIAja&6?yEN?(2k?Z`)VY{cCL83|5wGumav+c)zOCUi&(b+)S$&uZWlzV)X6S-N#+Q5Co`5^|6I zlf3{qo$y{n-@@Tx*?3_e(v}F$fD1u!SL+GQW;1zxM3!*04?ZOj9bZtTuoAj7)7e9O zEqXnA3p85$L6;$pJ6iRv)Hr>#zM|&H6mc}_9Zn|ynC6@2D#tc5Sg-=wH-l7AHC9@n z@<>sJxS2M8SXy2l2Bed5$SOMTz8B93>wEe^CxqK^ejdM9z`Se#^|^EURQhn=h}+L? z=B?7>dA-(U!&Y`r>l$l8KsYlhV~_C;KluGKxY8@5`^k)tqalx z)Q^s;n6*m(X$G141;XeouN*xj6Kq2JunYd`Op8=w&+Zh(a{~x=+*{tnH+9KhFfl=Z z*1-eQJlOe0BezdQJ8J)C(7q?vRlYjI z^7m4G*0YOWxP0agZMgTsK)Ff5GhKvmrw9DLSX7SShhO_N9kJYXoG!r~UhoTqF-(_x zysWSQrTCI|-aHY|zZ6}L>ln?mzC94YBALsc@Qr}p-~zFxNoKq@ov6D9v+$mROT-d_IO0#D2w?;SvyZU=pfTl)>G=A|@A-rcOq-u$8L zUB6o(AkNn5JodhvF-7){G&%3@y$+vgx1j*NxE`X|Kb1ru`3L@@(w4ry5seU&H?wpV zh5=2Fn(0zR(b2(zg%W=`&Py3M*ncK+j4=SOf9*s}5~JLLXy+9`tYoApb6qaNs*}(9u9#|1-Mc?!8|% zKK^$xV}aC{>$5!oVU8Cba$A~N?(PulRbvHfosnO|(t_P{Y?bKs5ge$AcC4OKc;QV4 z=jv-NNMYjn9Z0mg|M~N@(b3kF-8Z1`7rzCNm5V<|uF%A~Ztq3fN{vvY8EqSnrjbn0i4#i~6Dw<(UlX7WH4+DB|f>)?sx>zy2<5@Z%ob z7HVG+o_kY1KhFdu<_3%s%Znt3hwLR4mbL{H&cQ^9Vnw@zsqIpe(ymX}SjxYA8MIdF z7q_#yckjV01?I`m%Py(wxNzInkdMB1mN&i(k})tqA{FjqqtIKU<9gpvcf+Qs@=RX8 zpEb@Xq7390`pNckH?CC{E8j6ij-jHb1fSK^(m5>LW2V-Y=g6Zg^}^<&xrFClB6eL(dSni=Pr6*1k-WS3*jShqE)XP*;;9;HD|^&XzmIeE1UgiWeqt)&7W|C?$7ilTx)iu69= zJ`QLo*v$rk@WbFzGz2=L12s4sybl+GJ*81x=}@yJ}D0e7)Xn`!3!@ z+s83r#wsU0WjFFqGc76?O|umh;l9Fr(I|dNXM)>DMRpYbVhcT`OYAp7MhdU0<2BR z;M1pZ37TUCa(?dQQ&TLpQuLd_SVlk6)*EJJa%2C5yfy$+V&Y z`bC~1S%vr8-WVXuHWq<@^2ScjzBt+^^X|uNUb^%iRtcb)<}E zfLe|j(3$`^+dR%>KtJ_$M^@cREdENI+(k|FtozP4SB6V{XY=-!{D&vKir|Y)%vB9) z;l8a!$P|<;V=UewwWXHdQ7RDiJg&*UU2CMReXe!cTB*u$5m*GD!I@QhI z;w>gk3MjZvyfC1EW95lCm!c^_{$tMhRyrw0NSFEGkdEEw)_dOR82Z9{LNX8+MO~PS z_gsa3)e}k#juXTh!lVP*=4TXM%=AEW-)c%eg8j(oo#65hqBMXdac4zqs?l5Lv$$kQM1;Z(8I{k%!tFu$$ge}5 zup-5k2i9%yy<`1LsXT9;jnQf9br|DjtYFSzs1ek9wwjS1s2e8jWL*^S#a*5~J?$zr=b5O+p zv7ms7P~!n2^LIXbr+ZZ&RyRJ0^S=)bRoCCl&x%q4-w_#fe8h5n!W6ca#=Eb3tk_Ae zz&1WOGF26EqTj!^L9?^E|K{0091j!Zqg-0{t>O=g;Ec5+0LR?@#JD0y&+P_7tK11I zr&!pNJ=_UmWK+%mO*oe)Ya054W%zWa*fb;CvcB_5{CE==0!wYC2)uIODrR2}$a{`0 zMa3&Ay8)+Qs|R$Bc;dq+{L{sDrCk7!;DOc1_z#i~fWpV>utV*|@Kj)CDq)Pv}+abumKnfF;c>gvqugM!gwPjqT{pJ$enlA>mOBN8*aZ`$m+A9 zj41GAN;G1xQ8c}?S??*yVWJky)F#KgTO-V4`$HW5cv@q~^MXX7SX}BBvh_TN2&Y+ zil-d(mHlvs3%&J#-LM^g3w%A2a+&r>FgJ1RCBVbQR^>?v@2X;hMuH}tNq6|s>wPph7(#< zTrD{wT``_(>T!m5!L2wX@|X0q(#?8Qi8EQ9I1vFXR? zn^OdkGW|eU=579%Rz7`CA0JObBwVwFcC2y*h~4)|=u;1%=#rL~*-^(fjog8xZ<_}B z2LqBRnK2UO_Ua~EeS;xeic0ge(G<$^@rVFFO>Cu*OWShQ&{K+@(^MLb%XW1QmeNkLi&%vPIE`~&ep$~ zzQyu={#P>TD7-;FeW~yo~Vd$NfitO9J1{kynThB-W0FzHSidJCg5PWGlt&ms)8+4G1Yo)P&LSyO-E6IM8ty0_Ae_IFocK-NB5{KBs)`3B7+Pb# zX7J;XN=mH)oPRpoJmZZxy=7!l_oR9?5)f|DXIyoR{GL10lYR%$B-&RK>rjEvye%zE z)udT|sR*8Mi`#Q9aX$`(JFUYytv@mH~EER$8>CD5ac!T(6aQ5l9J zDuxk-?m3G~PFa24t)AQiq68X$u^9_?>4$iy1Tn` zNK1<%N;gP1NNj-84N5l>1|cdb43LrzDJcQz9v$1a?{9d2@AKTxIp@BP7Tf~&y>Ofk z1qG`JdThNktnvSc!A>g}e<3g*WZ0ED3y?%G)T1WkMtyi7FEL#2j zyW7V(ZRqX0^7Yh)*w3GtaL`8T9tZoP_;&d4R$ZuQWOgyhBsVzd9$dkqoL`Fla8gY5 zwvfv{G;ymyIMqyk%C<3Z#e+ecB$IzSKm{ZqscfOA>R%PbNmSszOyPe({~2u5`nY< zBFxpDA~%Pm?ll`O&-|vk86jddpNmG{&lY3a9s>^VJ~q2%SCzI<=^7|FqlRQ94xIzd zJ^P;VG5EyHKUWFLPiKi1OjA4N`Cwk=|Bj{h!rR8{TW|`)Eji$vpgLBOS2FLSjH}%} z0!?vh1A)c7lrW)}mjPLq5+vA!^nY}Cnk+^PGFu@x>nf9jabz7aNrjQ(AP4v80jAqI z?wmvn*>C-4nmGnB@xA@)PmBWiA-b&nI?+Py6nS6z6J2KM!bqqs3k_oV$#ZxjrOC9L zbIt<>koQ$p=dX$8R+?5r#?nE+Z&sX?ozP<(xb?$UJ*4^D@A(Pcff1Me{Bw$zb z<@HS$MJckc&p3W7eiZkKUZ)!*Y5x17v$1WAgRxN(2Qvd|ZUKs*Pa=pMdM+=34A%%p zuMPQAxrn`||3tUuQGzl|olEp9Ld6jH%Jlu3UhjXEriOa|#qIFhQA!YxjxG^^dCd#2 z9UnM{_&C~xEpT%Yz>w?L^WAvXCuV#rA0$fXdO=7CGEOnnd`6Eghep zB+&LCXjy5wgMpuz&_(^Tf&0*)X)nVF+o#yfTj8K~)grD3mMaRzJ9~ryA_QA^-ySU< zjzQ4CX>pTwAKFznTvMjl6T?W9XjYL^`i5wke`F<)MOt;H2uKc;QL-mMsaLn^g`g>G z&5tW5YaC|en?4MMFYU4NW8GaRhIdhrbx!5VOK*oWrauI!BwWJ7->kE2%;M2u-MG=l z2`3RFIK5M{Kr*!=)xEt8pD>WcRs*Nh+3Ntqq|{>jJQov+tQ}5AtSNjtvLxVZ*qL?u zol0HI|03xbC$!0p-}5Y7-T7;1aw6s-N31~W>lX5RXS6u^+D&W#)`YUUzn+yiw?-ok z&kc9)*4+eOiy>7s2_JT96QK*y4356nB1LpfscP9*qq!&FF*juraOZFygZt^=yTL_# z>htyJcdh7c2k>1Fg5;7Hu3C)+90$hZ8AU#4@ca%bdK;K69Kjp3f6J}Z;ZzmE4%nOg zqg*M*1+vmQA<#On=^QM$I`dEV#VrtZDb6u z53OA|3mF(Z>odJB{w>eI6*o%|F!cOBX?aYQeaG2uh>K!X1O5qwaUtRPL%J`MFP0V- z2-2B(^rBW5Iz?56I>0|JOyw_FQ9y?KOuRE#aH@lo`hV?z`@;bhKpeCeP&OFP!6o%| zbZbEhR#vML-STX}Jjk^n5&IS+H)vm=EtKB;QK3T}xO>@u`^Y|^s--orO;9MWz_Ij5 z9_|Q!_`|7B=_*$7opWDufHbM&zrcwqPWOl07TKr9gXz@^#ahFyy8w`~gp&HE&H{^INz@})WxC%hfA^{N4y>=g}3#`&N z8&vSz8J4)nay*XaU_X{$QVip2DXL75w{k5D5EA>j7p${+J0uY>@nP(KiE}yWE3z!mNx*C zk4H{@`GOue6Sj^G_MC}@{|;$U4>lugD*}SmM^_|yhEMIm3Ij7Z+YN~}mVmonO%|n! z#^%xH)cpmqstUk|up;z@y>f3uJ;f>Cep$P0VT_558FwnaDmh&GG1B02rKF2xRK>Rh zzNL{EV_;yZVQBAJ@`zqhgVk+bhzvvSgH@kXigwH0@;=%^_Uup#>Mj&`t=M&wm0af( zAbmVS#4hsmH^!wj9hNbe%jsXI>;b7;!PeExn_*W!`i{2XF8EUnuO1A@4R5X=b!Q{S zE^nX1l0sO!RwZ;$rC3*b$m5*&mC|A*4zOA;@kb-iXYT#Qp2x3`XYnxE(nr7f3_h_H z1$OYx=SKK}C=g+49N!49NH5rgf9p)!yDKp+2)~zU#?mwXh=q}{CUtrT ze2m_!+u9Y`z4^Ztx6Q^(5a}!&5YVpMiv3hb;DJZ>;DJhL{MnMCio~ej`gzQ^6M6>6 z?0rnu&C#RxvRrMm#3CQNm}92U(YodqV|SE0Q2(uFvKk zeH#Sn`((yDf2o>3V1%Ow>pI5|(q9Fmg+Y^CIO11LJXmLSQ?dZS50J>u0sJ|I+%d7K zDF@(xpf`av`Vgo2T(lWU+sL&QoV!^q*`AvRg(YjT0FLCr%;%+>q#=Xp(~IyKg7uk z_)>xt!O%5o*ck7QxRQIz2%pb*@5C6-7H^!PQ(>smA|=>hV;w7w=pT7`>F&p)Rubrc zX7xKZX3|p9Cfqr6TZX;-L9f|tJN%rJ3r1+Ji)~N-l+a4Tz3D{Vdl9LNp-CaoYN`ET z-TcN0HV>Qu+!~-BZ~p2qyK<>h&NaZ|G@Pzzv(#NtU_B+6ft{HOo?=3?{VI~=jyzy# zLJH?O4o&zS37I|X0yRyY->)eC93~lI<#Ec~cX8u)P3=`yO<*?MV>Cub>(A{dp`E(S z@&wmW`ZqAB@A_=m|KU~(eoQID8!oz=nc;8CuDx}os(p2a^jWQ&f1AKH_CfBA*1MeP z!LcFu@p0>HM`_Z>I-$)}WuSPTNJz{bz?4F3CeSZno% z;92M2^FFHmgoM7sKRQC1FyAC}@C(Lsv~K*%qIFNMYV;mkl=|QHVX)E9iJ-`hPC>%& zygY3EE?CY8eaWrd2C>*wd$2*5 zD34xIwOSqv&vY#3?oZxwv1MUQgDY z{O>WUN2}xeU-`?7ECVAB)*UQ~eGg{Bzl~yaKHb-ep%EUr_uG@$NrQ6-?+cb~cth?m z!Ne+7n;`#(%pZr($5+B$b5j{FZ%p$mNggTiDwY(%eIa9i3lOu~Wbl{2Lb0yGlROd+ zgrp=n;kan4uJ!}bVdbUK)&=tgGs*cpO#GeZ>ZtpbDmUc2d#b1Pc3^TqGWA{RnGwKi z4ARzlOL}+v{JBW%!G&!|aPV;BaN6GvuH#f}G3tI1tB$`0ba3LaJFT6hkDJ1y|14T? z<3&&W+Kr#+AXGN)(gYjta+WW7h0NF(I$OScVpeXiDk6j5{hJ6+=y~BVh?L6X(MqBQ zHUnPlvp*zy33$cfz({=V)LvuWeQQ?rdJc-6l!SodksbiilL(`wrIqOo4I zxtmcC{=a~I#cU13=r&(q(j;v4uamq)VCNF(xF!iMcl~2H`;V0u#}U^!d@AGu4|eay z$kMRPUO#8#Tu+6CK&@BoNPz(i=F#)GjJh1;_E84k-Ca~0>k3138)0(5LFZ^yb{HsaSYZG@@+e{W~4qw8l$G603a*INC5|;LKTBYM3PJKQ3RcZxofqIaa6(KCIJ7CKh56{Rz!Utf6K#;)KOeSpiJq~lN`*BY^g z#6hsg6llr9Om_-?M)aKrMnu%+jC(fut*Tgd5a5fBFQMxrTlVS+MTcv9BoDm&`?D}F zqD6bazuSRR^6)#xNc^{vp7B^s4!I(9N}4ij<^ZWL`GcM{Nx0_F5v2YLREu_jp&C_R zsW<)zWCvOHHFGn4=QsOBzeU}az_gz3Lp(40KU?|$pAs-bfVCga$gd5P+tLSNdL;6k zJG!8h_MmM+{L$`Oto%)6c>Z1n|kv zJzD^gFn;vNcsbk9X=9;jZTNVLg#`U;yk@n5xQeZc2c5zP2i&*(+S-(>6)nIrn zu{A~GMy5UcFbTBE_){75oH&Y(nM|8_oBEbE&oDs?>YA6sbk%#E`(TYG)2aCpK8RHe zE{t=W)B)2@z1(KjTPV#hjenz>nL%Hh3mabi02YnL7HqXDrn~zyiuM!ler50lvvRyr zqrZE{(vswt^FF!NMFJrd*x}U#G2#9E>kBGj(JcDtOXF~xtpY4L=51)|+(ixOrfx2|*!!X-XD`lg;#?R(;^G)I#4kjDp=?;26DAXMXd1Bh(Iwt}<0O0LY z_INTjT)sA$bhnUPe|K?Z(hwN@%ZC?J_wO_B9g{bxqJ5J3fSh7^FYE%pWfzIzn%GD7 z5uP!lr47TV?(4@@u(lv?1S{pbp6FH2IdKK|vGw#{t=XvFol}giNX%`E*}(wazJim8^IO_2GHoedEGU(_0r}IQx4 z&|#4NC>3kH7LaJekx=TG-7)*@+qkDe7_0h1ufp;rVO*=wJ3vybs$GW=!)1xoaNg%y zTD?IZE`=7EToO7c?Co+5d2XtVMrU<#tsyPn6Xa)UVA9_$5~LJ}z&pw4VE;1^{ZXb4 zBSHl;wq&F=jFJxsK7L_?VpxfN{lJ_6fn6vv%+zB+ZfQ_0#G1KO#ZWN>qy&>>%)fJF zo1BxFHQt=fKXDk37gSdCS1{C{`giRvoO}K9S#bEy(N-~Osxz>cGeU9TC;hSAF`VzN zT{=>hjNj;^jf#ymNO6@JYgiDZ`HnvG2@5GEAxY~koM_K)o)+k#cpJX2s= ze@EBRzjAAK0M_<@k#+hvqX6qY4>(Du@UPs1a(!s} zcU4CPU*sh@CC{-b!UecsXa|YDfJgz~?F+y24M?VCAGw6#|J=l}Tu~m!12jFMfrn`u zEVm`9@XVl3NDZ;dybL$(R=?9A(;Qg})<72C9-GBr*KUKY_)p#r2ut0Pf)XDwARPN! zthk=%jXki8iE_`y&NAx_-MB5y3gXkH ziw!{p#)szphpyBH!86}@YW@1<;!W|_=f|_871Z3;YJc{S2*p-yKe7$IU z?=LnQZr*O*vl#JQOGgeeV5tMjiHOceS%4HbDKO0@6MJ`u@o+%+Wi)K58em8m%^U|j zj`k3~b_Ch)^9T}rc4fz`M#A(oWBvD-fY#vfm(j>;cBA5abLpB<1nNk!4Ej&JFW{L` zF*Vp>_a z=Yo?uVp!nv{STBBe@>}KuH6$wfmG!63CQ8uL3=EMWZ_*B4d3+iOcZMkTZTQ_hDUuI5}aK~^Gi9-cX= z*27xZ`$jcOKFHLcTwBEu-v|)CQ<*7LnM$@9mqSd{V2v~JN2>l>cVe9@hk)Uv(9`b0 z`@d(T;(+gS|01?7>|=VL=d;Gg3c;0f7FWb`7cF?A4HQQ?_yGLdcOHcgXqC{F>lMum zhuo*Yk4$yCU!gVcr6n4JLec2iww1#ax2ktu#xcw0z*`y-@iP98Mu5&dzi1=o43uR5 z<>1sF9Mk&iiKJC$;BUpsoaWB^b6hZR=))FOcG}$P<6jOEWku67Sfy~4gRvQ`jWQdq z-!uUc#bc{;W$f57zO8>Z8ACxX;u4A+g^zBEQbv|&zT?Hb4ZQS#X+r5%E8jH4Kk|~7 zA)VXz)NiZ4vBi0uB?f%L^_0w?XPnxR98qqKxov=GZkr`f`9z1XgwYv^B2qzx5eKXW z=^{N?Zg*Oi-4DytGhd!TKq(3c<>rX{4|0CgHQ-UBH2eMS_VqA%4n^lY{7N`8r=v74 z*>gis|6pq$$&+EdM<;TjT;m}p$zbDV5#29Qen-|elycIC<)L_&e2G61TK=0_kmt)>SuUl zT%S@Nge|wr0;i8;v->=m7zd9k&U9+z%k8w76Hcs<-=j#7UV`gA`?ulJX=#h~*F97K z+`F7A0k$MGkPK#lGqw5zYM@sR8#D1_p3oM*zZ%x^f8#LdRd((CGWe1zd6ec$UEw^7 zqxF;~T_vne`zUpk(?92>IxfHXS^eB6x)hIHZO~K zizijTq}L(BWfa&tTh~MssjoFHJjVO0==S8DwU#Cq^cl0efM-ED2cj9JM`i!3LixwH zDUCp*jYj%*qCidosCg<93?(@F^XRWqPi2zd;)4#Yn+TVCFC#JKpdAXW0XDaG_Y;%U zNOcl3i?YF^_l{fOcK}|2wTd{I4TRag+`eVI(~O8fD_irrpyw8Jq}nz`;vL87D=eX) zKwJdy#~XO=99Fzh6Hs+dC*%@T`j{g?DiylttJP`Wd;5;z2JM>3!D_Z%BORR<}Yc+K>8CUy-d zhej(#g0(-gY^1}@p7w1vX!TtBC3TIZQO|y;28?sFMtWKV1mLM9z~~#s-jp&_=ocTZ z;)n~IVkq7=J^ECAIu?;b(%KV%+oA8_Z){Yg2+I{U194T2Khs`wsw&0T{_)KH#uqLd*!EvkL7*(u$0ZJ`xw+TOE`f>j-8TEI$^&;TLeo^wOA*sIY4sX#CYu}Y-*;)>zqvYP|2 zRtxeEs_Ut1e9neK{j?u|&h68U=JFnS7AX_pM56+h3CXm#a2%<<#h%KNIFefXmK+{$ z-#o%%XuawAdR=S->GfGYE3Yof67INR0mx_{X!*#VtmjyE2Ih8QR@TNS1y~QeM&lI8 zSjkOwyJyi8mtVv+=aNK)v#YJzgNs1)VIm2%+c}^!Zz|4zB{0@OT=891ul}R1Q1C5I zOq5MKxZ-~E=%b)@R|Gu;(Zppd)8*F>n-4YwZs;IY|fmETD zO3^0@iG#RK`VHG8p>DhHjFWX}t&@1nB!4^^dt=c}AR)SNM^v(P(1zV{391L3MKWSt zN2|0ok|ZuL&Rw z!_~4a_)*^lD(Pdja$H`MYg)gszQpN(Di{(E*hxTF^O_rYT)QZ8zqnLy=l=|L6&B~s z_yGX!dCO$~&^7+OR`nArihK9ZYK{So?;0BANTRss(TVzbfig>-nM4kHxq*yt^O6rK zXwbI`hkGMBhsP>qDQN46u;FQRTZPV#zSTwLWyy}C5-WaArS=oKn90qPv^-qAq2&Aeb#E6S_$b4mw6F$wle%;CzKJYTSGJ%V^c^6R zQ&AX_L`z`h0Az^SKAQ`f&0$mtIgb6BAd`wVU)l^A3){%JoWKPyB~iq=ob%yw|2;dl z8{_$%vV~eOP5|A~klZOgAbNcIPjV04C9B$$smyv1+VgdS?~Pt$t;n6#JEA#GAyZs^ zlGQQ3T8X1I%D1`XgZK76$qR9v&V~-4D|tOhEqqBi_m&*FXLt5VEqjEAD4zl_A6Mvm zW>zoS;!u^Yh{ALssauB_q6bKwwsd+gn{cojamm<+;EKieh^0kVY3dCn*&o`3yb4u$ zPwaD^QjDrHeYbf0aX*pBrb+af_h%Cu1>EOQ05D*|Gg9wO;HEoM5^9yR!Q&OyDIH?z z>-QKwRrado9sd!bd7g^%L7||f2f4(V|IV&-b_9HtccdiruA#~GbndfHzK)CGoBV#c5qnstfYT#9n#^>b@4C3= zGtbIn9c!G{m-P9pO$2I-BVqf}H>!*9?f~FBmWl3ytPmHh5Ff(1cfJi20d}o&1HG=8 zemZ2zNFA2}HB^su?9EDKe%pRxWN2h2mN)T40WNE*k|Y$riEc`3p2{}NT-ve@M9>-a zp%n$C2V#bomqBBp+CFewen7%`dz%*8T=mN=J`!cE5C}*!#rmU2je4kRi>uA{UazSD zK-CYUJ(;#|DaLoP47^Dl42_w)*PnA<)Rg8a%eOXQ7d}m>`}tB!^bhN{+u@$FqwJAI zWv|+!=PAF(`3wTuE1`ay+6txNx3xP4ehgKXe?B~-PiWPoVVhCnIwIp@?ETAPXq~fC zq$p*G3LX`MdFSAMqsVu-w;a)==A&tdx5(HWCyg6~Wy`#A`m$bXLsN~6n{^`^tFNS` zM;{Fypkw=2-W$U@0<9JF7BoaO3Eur%*s4A%qGq1`^ov|`dxiD{RY)}MssO;{3%1^k z8J#HnYh8_Nv443@m!chQ4Ise&DGIJMzn2)UW`Em!{-|IbOyxJ$_!@p{zpDj%PefO~ zA64zm@=GGYUq>9se9w^=;omtik2^x6p~8Mkb{lv-sd0EE26)dH!$Cj}M~AW`4wRzB)h3)??W_ht@=ty6R7O_qIZ zXd)I*A|Z(uy}KPUTtQ0sSM^wGglx#~%kuPZv;HaIRvr_V8$BB)4|;M;LI_>e1M^%- zQT56;@v=7Qa07J!j;zs>#0bT72gqVEZxI#X!yQ#Pt!)GowvBF6w8;khFatCnvsh<% zAL4TVUGS~UN~UsH8w0(Ff6k?MKV4{9jt|h*8#DhNgEk+7-@};Wy3hgyA;UJd$`sbErPo}dW=0+hHUO!3PjS6XviDx z91U&KDtL28Udx)A3}jvYd(Iis^)1Pt2rg_R&ZV|yUnW+4b^Tf(fp-Th7xs2*XYTWo zEKK;_Q|Qpo5a`!8>6KP|ImO!F2GhOD`Q_;Vb1mzY}*3;+Cg(sOrRN3Qvdk`kw89|Ptf+Cr?R~@Q{ zvsd(Rc+U(Q4NHq~`7?){EP0Dzi6O?pFjlDRiE-DQz~&k(j&H`BaEr*><#kno0d1k2 zi$4!fzh!nxhgT9jDl-&EJE?%0Dx%3mTAHQtL|bC~DUu}{0>ow97Pj6GbZcIsxmxZz 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 BTr~gy literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..892a7dfbe1443af7496d29643ea38f33ab1895c1 GIT binary patch literal 257096 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 z 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

wwdm!fJ%u#g?Em!1-ed`yfPZ!?%4qWLc04z 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

xM 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 Zg+iIHI)8tl0s{W1-_=$wzhn90{{S`wu1x>{ literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/rust-toolchain.toml b/apps/desktop/src-tauri/rust-toolchain.toml new file mode 100644 index 000000000..bf0cec700 --- /dev/null +++ b/apps/desktop/src-tauri/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.96.0" +components = ["clippy", "rustfmt"] +profile = "minimal" +targets = ["aarch64-apple-darwin"] diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 000000000..a2d410494 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,421 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; +use sha2::{Digest, Sha256}; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::RwLock; +use tauri::async_runtime::{Mutex, Receiver}; +use tauri::{AppHandle, Emitter, Manager, State}; +use tauri_plugin_dialog::DialogExt; +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; + +const MAX_EDITABLE_FILE_BYTES: u64 = 5 * 1024 * 1024; + +#[derive(Default)] +struct WorkspaceState { + root: RwLock>, +} + +#[derive(Default)] +struct CoreService { + inner: Mutex, +} + +#[derive(Default)] +struct CoreServiceState { + process: Option, + next_request_id: u64, +} + +struct CoreProcess { + child: Option, + events: Receiver, +} + +impl Drop for CoreProcess { + fn drop(&mut self) { + if let Some(child) = self.child.take() { + let _ = child.kill(); + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileDocument { + path: String, + content: String, + revision: String, +} + +#[derive(Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +enum CoreMessage { + Event { + event: String, + #[serde(flatten)] + details: Map, + }, + Response { + id: Option, + #[serde(flatten)] + outcome: CoreResponseOutcome, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "outcome", rename_all = "lowercase")] +enum CoreResponseOutcome { + Success { result: Value }, + Error { error: String }, +} + +fn sha256_hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn canonical_workspace_root(path: &Path) -> Result { + let root = path + .canonicalize() + .map_err(|error| format!("Unable to open workspace: {error}"))?; + if !root.is_dir() { + return Err("The selected workspace is not a folder.".to_string()); + } + Ok(root) +} + +fn resolve_workspace_file(root: &Path, relative_path: &str) -> Result { + let relative = Path::new(relative_path); + if relative.as_os_str().is_empty() || relative.is_absolute() { + return Err("File path must be relative to the active workspace.".to_string()); + } + let canonical_root = root + .canonicalize() + .map_err(|error| format!("Unable to resolve workspace: {error}"))?; + let candidate = canonical_root + .join(relative) + .canonicalize() + .map_err(|error| format!("Unable to resolve File: {error}"))?; + if !candidate.starts_with(&canonical_root) { + return Err("File path leaves the active workspace.".to_string()); + } + if !candidate.is_file() { + return Err("The selected path is not a File.".to_string()); + } + Ok(candidate) +} + +fn active_workspace_root(state: &WorkspaceState) -> Result { + state + .root + .read() + .map_err(|_| "Workspace state is unavailable.".to_string())? + .clone() + .ok_or_else(|| "Open a workspace first.".to_string()) +} + +fn read_file_document(root: &Path, relative_path: &str) -> Result { + let file_path = resolve_workspace_file(root, relative_path)?; + let metadata = file_path + .metadata() + .map_err(|error| format!("Unable to inspect File: {error}"))?; + if metadata.len() > MAX_EDITABLE_FILE_BYTES { + return Err("This File is larger than the 5 MiB editor limit.".to_string()); + } + let mut bytes = Vec::with_capacity(metadata.len() as usize); + File::open(&file_path) + .and_then(|mut file| file.read_to_end(&mut bytes)) + .map_err(|error| format!("Unable to read File: {error}"))?; + if bytes.contains(&0) { + return Err("Binary Files cannot be opened in the text editor.".to_string()); + } + let content = String::from_utf8(bytes.clone()) + .map_err(|_| "This File is not valid UTF-8 text.".to_string())?; + Ok(FileDocument { + path: relative_path.to_string(), + content, + revision: sha256_hex(&bytes), + }) +} + +fn save_file_document( + root: &Path, + relative_path: &str, + content: &str, + expected_revision: &str, +) -> Result { + let file_path = resolve_workspace_file(root, relative_path)?; + let existing = fs::read(&file_path).map_err(|error| format!("Unable to read File: {error}"))?; + if sha256_hex(&existing) != expected_revision { + return Err("The File changed outside CodeGraphy. Reopen it before saving.".to_string()); + } + if content.len() as u64 > MAX_EDITABLE_FILE_BYTES { + return Err("This File is larger than the 5 MiB editor limit.".to_string()); + } + let parent = file_path + .parent() + .ok_or_else(|| "Unable to resolve the File folder.".to_string())?; + let permissions = file_path + .metadata() + .map_err(|error| format!("Unable to inspect File: {error}"))? + .permissions(); + let mut temporary = tempfile::NamedTempFile::new_in(parent) + .map_err(|error| format!("Unable to create a safe save File: {error}"))?; + temporary + .write_all(content.as_bytes()) + .and_then(|_| temporary.flush()) + .and_then(|_| temporary.as_file().sync_all()) + .map_err(|error| format!("Unable to write File: {error}"))?; + temporary + .as_file() + .set_permissions(permissions) + .map_err(|error| format!("Unable to preserve File permissions: {error}"))?; + temporary + .persist(&file_path) + .map_err(|error| format!("Unable to replace File safely: {}", error.error))?; + read_file_document(root, relative_path) +} + +fn spawn_core_process(app: &AppHandle) -> Result { + let script = app + .path() + .resource_dir() + .map_err(|error| format!("Unable to locate app resources: {error}"))? + .join("runtime") + .join("sidecar.mjs"); + let (events, child) = app + .shell() + .sidecar("codegraphy-core") + .map_err(|error| format!("Unable to locate Core service: {error}"))? + .arg(script) + .spawn() + .map_err(|error| format!("Unable to start Core service: {error}"))?; + Ok(CoreProcess { + child: Some(child), + events, + }) +} + +async fn request_core( + app: &AppHandle, + service: &CoreService, + method: &str, + workspace_root: &Path, + include_symbols: bool, +) -> Result { + let mut state = service.inner.lock().await; + state.next_request_id += 1; + let request_id = state.next_request_id; + if state.process.is_none() { + state.process = Some(spawn_core_process(app)?); + } + let process = state + .process + .as_mut() + .ok_or_else(|| "Core service is unavailable.".to_string())?; + let request = json!({ + "kind": "request", + "id": request_id, + "method": method, + "params": { + "workspaceRoot": workspace_root, + "includeSymbols": include_symbols, + }, + }); + let mut request_bytes = serde_json::to_vec(&request) + .map_err(|error| format!("Unable to encode Core request: {error}"))?; + request_bytes.push(b'\n'); + process + .child + .as_mut() + .ok_or_else(|| "Core service stopped.".to_string())? + .write(&request_bytes) + .map_err(|error| format!("Unable to send Core request: {error}"))?; + + while let Some(event) = process.events.recv().await { + match event { + CommandEvent::Stdout(line) => { + let message: CoreMessage = serde_json::from_slice(&line) + .map_err(|error| format!("Core service returned invalid data: {error}"))?; + match message { + CoreMessage::Event { event, mut details } => { + details.insert("event".to_string(), Value::String(event)); + app.emit("core-service-event", details) + .map_err(|error| format!("Unable to report Core progress: {error}"))?; + } + CoreMessage::Response { id, outcome } if id == Some(request_id) => { + return match outcome { + CoreResponseOutcome::Success { result } => Ok(result), + CoreResponseOutcome::Error { error } => Err(error), + }; + } + CoreMessage::Response { .. } => { + return Err("Core service returned a mismatched response.".to_string()); + } + } + } + CommandEvent::Stderr(line) => { + let diagnostic = String::from_utf8_lossy(&line).trim().to_string(); + if !diagnostic.is_empty() { + let _ = app.emit("core-service-diagnostic", diagnostic); + } + } + CommandEvent::Error(error) => { + state.process = None; + return Err(format!("Core service failed: {error}")); + } + CommandEvent::Terminated(terminated) => { + state.process = None; + return Err(format!( + "Core service stopped with code {:?}.", + terminated.code + )); + } + _ => {} + } + } + state.process = None; + Err("Core service stopped before it returned a response.".to_string()) +} + +#[tauri::command] +async fn choose_workspace(app: AppHandle) -> Result, String> { + let selected = app.dialog().file().blocking_pick_folder(); + selected + .map(|file_path| { + file_path + .into_path() + .map_err(|error| format!("Unable to read selected workspace: {error}")) + .and_then(|path| canonical_workspace_root(&path)) + .map(|path| path.to_string_lossy().into_owned()) + }) + .transpose() +} + +#[tauri::command] +fn initial_workspace() -> Option { + std::env::var("CODEGRAPHY_DESKTOP_WORKSPACE") + .ok() + .filter(|value| !value.is_empty()) +} + +#[tauri::command] +async fn load_workspace_graph( + app: AppHandle, + core: State<'_, CoreService>, + workspace: State<'_, WorkspaceState>, + workspace_root: String, + reindex: bool, + include_symbols: bool, +) -> Result { + let root = canonical_workspace_root(Path::new(&workspace_root))?; + { + let mut active_root = workspace + .root + .write() + .map_err(|_| "Workspace state is unavailable.".to_string())?; + *active_root = Some(root.clone()); + } + request_core( + &app, + &core, + if reindex { "index" } else { "open" }, + &root, + include_symbols, + ) + .await +} + +#[tauri::command] +async fn read_workspace_file( + workspace: State<'_, WorkspaceState>, + relative_path: String, +) -> Result { + let root = active_workspace_root(&workspace)?; + tauri::async_runtime::spawn_blocking(move || read_file_document(&root, &relative_path)) + .await + .map_err(|error| format!("Unable to read File: {error}"))? +} + +#[tauri::command] +async fn save_workspace_file( + workspace: State<'_, WorkspaceState>, + relative_path: String, + content: String, + expected_revision: String, +) -> Result { + let root = active_workspace_root(&workspace)?; + tauri::async_runtime::spawn_blocking(move || { + save_file_document(&root, &relative_path, &content, &expected_revision) + }) + .await + .map_err(|error| format!("Unable to save File: {error}"))? +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .manage(CoreService::default()) + .manage(WorkspaceState::default()) + .invoke_handler(tauri::generate_handler![ + choose_workspace, + initial_workspace, + load_workspace_graph, + read_workspace_file, + save_workspace_file, + ]) + .run(tauri::generate_context!()) + .expect("error while running CodeGraphy desktop"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_files_outside_the_active_workspace() { + let workspace = tempfile::tempdir().expect("workspace"); + let outside = tempfile::NamedTempFile::new().expect("outside File"); + let link = workspace.path().join("outside-link.ts"); + std::os::unix::fs::symlink(outside.path(), &link).expect("symlink"); + + let result = resolve_workspace_file(workspace.path(), "outside-link.ts"); + + assert_eq!( + result.unwrap_err(), + "File path leaves the active workspace." + ); + } + + #[test] + fn atomic_save_detects_external_changes() { + let workspace = tempfile::tempdir().expect("workspace"); + let file_path = workspace.path().join("entry.ts"); + fs::write(&file_path, "export const value = 1;\n").expect("write File"); + let opened = read_file_document(workspace.path(), "entry.ts").expect("open File"); + fs::write(&file_path, "export const value = 2;\n").expect("external edit"); + + let result = save_file_document( + workspace.path(), + "entry.ts", + "export const value = 3;\n", + &opened.revision, + ); + + assert_eq!( + result.unwrap_err(), + "The File changed outside CodeGraphy. Reopen it before saving." + ); + assert_eq!( + fs::read_to_string(file_path).expect("read File"), + "export const value = 2;\n" + ); + } +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 000000000..17392c774 --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + codegraphy_desktop_lib::run(); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 000000000..06b702a2b --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "CodeGraphy", + "version": "0.1.0", + "identifier": "dev.codegraphy.desktop", + "build": { + "beforeDevCommand": "pnpm run build:sidecar && pnpm exec vite", + "beforeBuildCommand": "pnpm run build:sidecar && pnpm run build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "security": { + "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'" + }, + "windows": [ + { + "title": "CodeGraphy", + "width": 1440, + "height": 900, + "minWidth": 1040, + "minHeight": 680, + "center": true, + "decorations": true, + "hiddenTitle": true, + "titleBarStyle": "Overlay" + } + ] + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "Copyright © 2026 Joe Soboleski", + "externalBin": ["binaries/codegraphy-core"], + "icon": ["icons/icon.icns"], + "longDescription": "A fast local code navigator for Files, Symbols, and their Relationships.", + "macOS": { + "entitlements": "Entitlements.plist", + "hardenedRuntime": true, + "minimumSystemVersion": "26.0" + }, + "resources": ["runtime/"], + "shortDescription": "Local Relationship Graph for code", + "targets": ["app", "dmg"] + } +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 000000000..029d72e4d --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,212 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + chooseWorkspace, + initialWorkspace, + loadWorkspaceGraph, + readWorkspaceFile, + saveWorkspaceFile, + type FileDocument, +} from './bridge'; +import { CodeEditor } from './components/CodeEditor'; +import { FileTree } from './components/FileTree'; +import { GraphPanel } from './components/GraphPanel'; +import { buildFileTree, type WorkspaceGraphResult } from './model'; + +type AppStatus = + | { kind: 'idle'; message: string } + | { kind: 'busy'; message: string } + | { kind: 'ready'; message: string } + | { kind: 'error'; message: string }; + +function displayWorkspaceName(workspaceRoot: string): string { + return workspaceRoot.split('/').filter(Boolean).at(-1) ?? workspaceRoot; +} + +export function App(): React.ReactElement { + const [workspaceRoot, setWorkspaceRoot] = useState(); + const [graphResult, setGraphResult] = useState(); + const [status, setStatus] = useState({ + kind: 'idle', + message: 'Open a local workspace to begin.', + }); + const [includeSymbols, setIncludeSymbols] = useState(false); + const [document, setDocument] = useState(); + const [draft, setDraft] = useState(''); + const [saving, setSaving] = useState(false); + const graph = graphResult?.kind === 'ready' ? graphResult.graph : undefined; + const tree = useMemo(() => graph ? buildFileTree(graph) : [], [graph]); + const dirty = document !== undefined && draft !== document.content; + + const openGraph = useCallback(async ( + root: string, + options: { reindex: boolean; symbols: boolean }, + ): Promise => { + setStatus({ + kind: 'busy', + message: options.reindex ? 'Core is re-indexing the workspace…' : 'Loading the Graph Cache…', + }); + try { + const result = await loadWorkspaceGraph({ + workspaceRoot: root, + reindex: options.reindex, + includeSymbols: options.symbols, + }); + setWorkspaceRoot(result.workspaceRoot); + setGraphResult(result); + if (result.kind === 'unreadable') { + setStatus({ kind: 'error', message: result.message }); + return; + } + const indexing = result.indexing + ? `${result.indexing.mode} Indexing · ${result.indexing.analyzedFiles} analyzed · ${result.indexing.reusedFiles} reused` + : result.cacheStatus.state === 'stale' ? 'Graph Cache is stale · Re-index when ready' : 'Graph Cache is current'; + setStatus({ kind: 'ready', message: indexing }); + } catch (error) { + setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); + } + }, []); + + useEffect(() => { + void initialWorkspace().then((root) => { + if (root) void openGraph(root, { reindex: false, symbols: false }); + }).catch(error => setStatus({ + kind: 'error', + message: error instanceof Error ? error.message : String(error), + })); + }, [openGraph]); + + const handleOpenWorkspace = async (): Promise => { + try { + const root = await chooseWorkspace(); + if (!root) return; + setDocument(undefined); + setDraft(''); + await openGraph(root, { reindex: false, symbols: includeSymbols }); + } catch (error) { + setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); + } + }; + + const handleSelectFile = async (path: string): Promise => { + if (document?.path === path) return; + if (dirty && !window.confirm('Discard the unsaved edit and open another File?')) return; + try { + const next = await readWorkspaceFile(path); + setDocument(next); + setDraft(next.content); + } catch (error) { + setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); + } + }; + + const handleSave = async (): Promise => { + if (!document || !dirty || saving || !workspaceRoot) return; + setSaving(true); + try { + const saved = await saveWorkspaceFile({ + relativePath: document.path, + content: draft, + expectedRevision: document.revision, + }); + setDocument(saved); + setDraft(saved.content); + await openGraph(workspaceRoot, { reindex: true, symbols: includeSymbols }); + } catch (error) { + setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); + } finally { + setSaving(false); + } + }; + + const handleSymbolToggle = async (): Promise => { + const next = !includeSymbols; + setIncludeSymbols(next); + if (workspaceRoot) await openGraph(workspaceRoot, { reindex: false, symbols: next }); + }; + + return ( +
+
+
+
+ + CodeGraphy + {workspaceRoot ? / {displayWorkspaceName(workspaceRoot)} : null} +
+
+ + + +
+
+ + {graph ? ( +
+ + +
+
+ {document?.path ?? 'Editor'} + +
+ {document ? ( + void handleSave()} /> + ) : ( +
+ {`{ }`} +

Choose a File

+

Browse the workspace hierarchy, make a lightweight edit, and save it back to the source File.

+
+ )} +
+ + +
+ ) : ( +
+
+

MACOS-FIRST · LOCAL BY DESIGN

+

See how your code
fits together.

+

Open any local folder. CodeGraphy will build a workspace-owned Graph Cache, then put its Files, editor, and Relationships in one focused view.

+ +
+ )} + +
+ + {status.message} + Local only · macOS 26+ +
+
+ ); +} diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts new file mode 100644 index 000000000..5ef695931 --- /dev/null +++ b/apps/desktop/src/bridge.ts @@ -0,0 +1,57 @@ +import { invoke } from '@tauri-apps/api/core'; +import { parseWorkspaceGraphResult, type WorkspaceGraphResult } from './model'; + +export interface FileDocument { + path: string; + content: string; + revision: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function parseFileDocument(value: unknown): FileDocument { + if (!isRecord(value) + || typeof value.path !== 'string' + || typeof value.content !== 'string' + || typeof value.revision !== 'string') { + throw new Error('The desktop host returned an invalid File.'); + } + return { path: value.path, content: value.content, revision: value.revision }; +} + +export async function chooseWorkspace(): Promise { + const result = await invoke('choose_workspace'); + if (result === null) return undefined; + if (typeof result !== 'string') throw new Error('The desktop host returned an invalid workspace.'); + return result; +} + +export async function initialWorkspace(): Promise { + const result = await invoke('initial_workspace'); + if (result === null) return undefined; + if (typeof result !== 'string') throw new Error('The desktop host returned an invalid workspace.'); + return result; +} + +export async function loadWorkspaceGraph(input: { + workspaceRoot: string; + reindex: boolean; + includeSymbols: boolean; +}): Promise { + const result = await invoke('load_workspace_graph', input); + return parseWorkspaceGraphResult(result); +} + +export async function readWorkspaceFile(relativePath: string): Promise { + return parseFileDocument(await invoke('read_workspace_file', { relativePath })); +} + +export async function saveWorkspaceFile(input: { + relativePath: string; + content: string; + expectedRevision: string; +}): Promise { + return parseFileDocument(await invoke('save_workspace_file', input)); +} diff --git a/apps/desktop/src/components/CodeEditor.tsx b/apps/desktop/src/components/CodeEditor.tsx new file mode 100644 index 000000000..650cd15d9 --- /dev/null +++ b/apps/desktop/src/components/CodeEditor.tsx @@ -0,0 +1,103 @@ +import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; +import { javascript } from '@codemirror/lang-javascript'; +import { + bracketMatching, + defaultHighlightStyle, + indentOnInput, + syntaxHighlighting, +} from '@codemirror/language'; +import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; +import { EditorState } from '@codemirror/state'; +import { + drawSelection, + dropCursor, + EditorView, + highlightActiveLine, + highlightActiveLineGutter, + keymap, + lineNumbers, +} from '@codemirror/view'; +import { useEffect, useRef } from 'react'; +import type { FileDocument } from '../bridge'; + +function languageForPath(path: string) { + if (/\.[cm]?[jt]sx?$/iu.test(path)) { + return javascript({ + jsx: /x$/iu.test(path), + typescript: /\.[cm]?tsx?$/iu.test(path), + }); + } + return []; +} + +export function CodeEditor({ + document, + onChange, + onSave, +}: { + document: FileDocument; + onChange: (content: string) => void; + onSave: () => void; +}): React.ReactElement { + const hostRef = useRef(null); + const onChangeRef = useRef(onChange); + const onSaveRef = useRef(onSave); + onChangeRef.current = onChange; + onSaveRef.current = onSave; + + useEffect(() => { + const host = hostRef.current; + if (!host) return; + const view = new EditorView({ + parent: host, + state: EditorState.create({ + doc: document.content, + extensions: [ + lineNumbers(), + highlightActiveLineGutter(), + history(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + bracketMatching(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + highlightSelectionMatches(), + highlightActiveLine(), + languageForPath(document.path), + keymap.of([ + { key: 'Mod-s', run: () => { onSaveRef.current(); return true; } }, + indentWithTab, + ...defaultKeymap, + ...historyKeymap, + ...searchKeymap, + ]), + EditorView.lineWrapping, + EditorView.updateListener.of((update) => { + if (update.docChanged) onChangeRef.current(update.state.doc.toString()); + }), + EditorView.theme({ + '&': { height: '100%', backgroundColor: '#0c161d', color: '#dbe8ee' }, + '.cm-content': { caretColor: '#73e2c4', padding: '18px 0 40px' }, + '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#73e2c4' }, + '.cm-gutters': { + backgroundColor: '#0c161d', + borderRight: '1px solid rgba(149, 180, 190, .1)', + color: '#49616d', + }, + '.cm-activeLine': { backgroundColor: 'rgba(110, 221, 193, .045)' }, + '.cm-activeLineGutter': { backgroundColor: 'rgba(110, 221, 193, .07)', color: '#88a5af' }, + '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { + backgroundColor: 'rgba(69, 141, 168, .35)', + }, + '.cm-scroller': { fontFamily: 'var(--font-code)', fontSize: '12.5px', lineHeight: '1.68' }, + }), + ], + }), + }); + view.focus(); + return () => view.destroy(); + }, [document.path, document.revision]); + + return
; +} diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx new file mode 100644 index 000000000..4f8545063 --- /dev/null +++ b/apps/desktop/src/components/FileTree.tsx @@ -0,0 +1,53 @@ +import type { FileTreeEntry } from '../model'; + +function Entry({ + entry, + selectedPath, + onSelect, +}: { + entry: FileTreeEntry; + selectedPath?: string; + onSelect: (path: string) => void; +}): React.ReactElement { + if (entry.kind === 'file') { + return ( + + ); + } + return ( +
+ {entry.name} +
+ {entry.children.map(child => ( + + ))} +
+
+ ); +} + +export function FileTree({ + entries, + selectedPath, + onSelect, +}: { + entries: FileTreeEntry[]; + selectedPath?: string; + onSelect: (path: string) => void; +}): React.ReactElement { + return ( + + ); +} diff --git a/apps/desktop/src/components/GraphPanel.test.tsx b/apps/desktop/src/components/GraphPanel.test.tsx new file mode 100644 index 000000000..d00066caf --- /dev/null +++ b/apps/desktop/src/components/GraphPanel.test.tsx @@ -0,0 +1,74 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { DesktopGraph } from '../model'; + +const rendererMocks = vi.hoisted(() => { + let resolvePhysics: (() => void) | undefined; + const prepareGraphPhysics = vi.fn(() => new Promise((resolve) => { + resolvePhysics = resolve; + })); + return { + createGraphLayoutEngine: vi.fn(() => ({ + edgeSources: new Uint32Array(), + edgeTargets: new Uint32Array(), + pause: vi.fn(), + tick: vi.fn(() => ({ settled: true, steps: 0 })), + x: new Float32Array([0]), + y: new Float32Array([0]), + })), + prepareGraphPhysics, + resolvePhysics: () => resolvePhysics?.(), + }; +}); + +vi.mock('@codegraphy-dev/graph-renderer', () => ({ + createGraphLayoutEngine: rendererMocks.createGraphLayoutEngine, + prepareGraphPhysics: rendererMocks.prepareGraphPhysics, + WebGpuGraphRenderer: { + create: vi.fn(async () => undefined), + }, +})); + +import { GraphPanel } from './GraphPanel'; + +const graph: DesktopGraph = { + edges: [], + nodes: [{ id: 'src/index.ts', label: 'index.ts', nodeType: 'file' }], +}; + +describe('GraphPanel', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + vi.stubGlobal('ResizeObserver', class { + disconnect(): void {} + observe(): void {} + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('prepares the renderer WASM before it creates the layout engine', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + + await act(async () => { + root.render( undefined} />); + }); + + expect(rendererMocks.prepareGraphPhysics).toHaveBeenCalledOnce(); + expect(rendererMocks.createGraphLayoutEngine).not.toHaveBeenCalled(); + + await act(async () => { + rendererMocks.resolvePhysics(); + }); + + expect(rendererMocks.createGraphLayoutEngine).toHaveBeenCalledOnce(); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/components/GraphPanel.tsx b/apps/desktop/src/components/GraphPanel.tsx new file mode 100644 index 000000000..ab68979fb --- /dev/null +++ b/apps/desktop/src/components/GraphPanel.tsx @@ -0,0 +1,289 @@ +import { + createGraphLayoutEngine, + prepareGraphPhysics, + WebGpuGraphRenderer, + type GraphRendererLink, + type GraphRendererNode, + type GraphRendererNodeStyle, +} from '@codegraphy-dev/graph-renderer'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import type { DesktopGraph, DesktopGraphNode } from '../model'; + +function nodeStyle(node: DesktopGraphNode, selectedId: string | undefined): GraphRendererNodeStyle { + const symbol = Boolean(node.symbol); + const selected = node.id === selectedId; + const folder = node.nodeType === 'folder'; + const size = selected ? (symbol ? 13 : 18) : (symbol ? 7 : 11); + return { + borderColor: selected ? '#f3fbff' : (symbol ? '#bb9cff' : '#a6e7d7'), + borderWidth: selected ? 2 : 1, + cornerRadius: folder ? 2 : 6, + fillColor: selected ? '#55d9b8' : (symbol ? '#7758b8' : folder ? '#c59555' : '#197c84'), + fillOpacity: 1, + height: folder ? size * 0.72 : size, + opacity: 1, + shape: folder ? 'rectangle' : symbol ? 'diamond' : 'circle', + width: folder ? size * 1.35 : size, + }; +} + +function fitCamera(x: Float32Array, y: Float32Array, width: number, height: number) { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + for (let index = 0; index < x.length; index += 1) { + const nodeX = x[index]; + const nodeY = y[index]; + if (nodeX === undefined || nodeY === undefined) continue; + minX = Math.min(minX, nodeX); + maxX = Math.max(maxX, nodeX); + minY = Math.min(minY, nodeY); + maxY = Math.max(maxY, nodeY); + } + if (!Number.isFinite(minX)) return { centerX: 0, centerY: 0, zoom: 1 }; + const zoom = Math.max(0.08, Math.min(2.2, Math.min( + width / Math.max(120, maxX - minX + 100), + height / Math.max(120, maxY - minY + 100), + ))); + return { centerX: (minX + maxX) / 2, centerY: (minY + maxY) / 2, zoom }; +} + +export function GraphPanel({ + graph, + selectedId, + onSelect, +}: { + graph: DesktopGraph; + selectedId?: string; + onSelect: (id: string) => void; +}): React.ReactElement { + const canvasRef = useRef(null); + const selectedIdRef = useRef(selectedId); + const redrawRef = useRef<(() => void) | undefined>(undefined); + const [physicsReady, setPhysicsReady] = useState(false); + const [rendererError, setRendererError] = useState(); + selectedIdRef.current = selectedId; + + const relationships = useMemo(() => graph.edges.filter( + edge => edge.from === selectedId || edge.to === selectedId, + ).slice(0, 8), [graph.edges, selectedId]); + + useEffect(() => { + redrawRef.current?.(); + }, [selectedId]); + + useEffect(() => { + let active = true; + void prepareGraphPhysics() + .then(() => { if (active) setPhysicsReady(true); }) + .catch(error => { + if (active) setRendererError(error instanceof Error ? error.message : String(error)); + }); + return () => { active = false; }; + }, []); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || graph.nodes.length === 0 || !physicsReady) return; + let active = true; + let animationFrame: number | undefined; + let framePending = false; + let continueAfterFrame = false; + let renderer: WebGpuGraphRenderer | undefined; + let fitFrames = 90; + let positionVersion = 0; + let styleVersion = 0; + let camera = { centerX: 0, centerY: 0, zoom: 1 }; + let pointer: { x: number; y: number } | undefined; + + const nodes: GraphRendererNode[] = graph.nodes.map(node => ({ id: node.id })); + const nodeById = new Map(nodes.map(node => [node.id, node])); + const sourceIndexes: number[] = []; + const targetIndexes: number[] = []; + const links: GraphRendererLink[] = []; + const indexById = new Map(nodes.map((node, index) => [node.id, index])); + for (const edge of graph.edges) { + const sourceIndex = indexById.get(edge.from); + const targetIndex = indexById.get(edge.to); + const source = nodeById.get(edge.from); + const target = nodeById.get(edge.to); + if (sourceIndex === undefined || targetIndex === undefined || !source || !target) continue; + sourceIndexes.push(sourceIndex); + targetIndexes.push(targetIndex); + links.push({ source, target }); + } + const layout = createGraphLayoutEngine({ + nodeIds: nodes.map(node => node.id), + radii: Float32Array.from(graph.nodes.map(node => node.symbol ? 4 : 7)), + chargeStrengthMultipliers: Float32Array.from(graph.nodes.map(node => node.symbol ? 0.75 : 1.2)), + edgeSources: Uint32Array.from(sourceIndexes), + edgeTargets: Uint32Array.from(targetIndexes), + }, { + chargeStrength: -85, + collisionPadding: 3, + initializationSpacing: 26, + linkDistance: 42, + linkStrength: 0.45, + settleSpeed: 0.05, + settleSteps: 36, + }); + + const schedule = (): void => { + if (!active || framePending) return; + framePending = true; + animationFrame = requestAnimationFrame(() => { + framePending = false; + draw(); + }); + }; + const getNodeStyle = (node: GraphRendererNode): GraphRendererNodeStyle => { + const index = indexById.get(node.id); + const source = index === undefined ? undefined : graph.nodes[index]; + return source + ? nodeStyle(source, selectedIdRef.current) + : nodeStyle({ id: node.id, label: node.id }, selectedIdRef.current); + }; + const draw = (): void => { + if (!renderer?.canRender() || !active) return; + const tick = layout.tick(); + continueAfterFrame = !tick.settled; + if (tick.steps > 0) positionVersion += 1; + for (let index = 0; index < nodes.length; index += 1) { + const node = nodes[index]; + if (!node) continue; + node.x = layout.x[index]; + node.y = layout.y[index]; + } + if (fitFrames > 0) { + camera = fitCamera(layout.x, layout.y, canvas.clientWidth, canvas.clientHeight); + fitFrames -= 1; + } + renderer.render({ + backgroundColor: '#09151b', + camera, + cssHeight: Math.max(1, canvas.clientHeight), + cssWidth: Math.max(1, canvas.clientWidth), + devicePixelRatio: window.devicePixelRatio, + directionMode: 'arrows', + edgeSources: layout.edgeSources, + edgeTargets: layout.edgeTargets, + getArrowColor: () => '#6fa9b3', + getLinkColor: () => '#345861', + getLinkOpacity: () => 0.7, + getLinkWidth: () => 0.8, + getNodeStyle, + hoveredLink: null, + hoveredNodeIndex: -1, + hoveredNodeScale: 1, + links, + nodes, + nodeX: layout.x, + nodeY: layout.y, + positionVersion, + styleVersion, + }); + }; + const redraw = (): void => { + styleVersion += 1; + schedule(); + }; + redrawRef.current = redraw; + const resize = new ResizeObserver(redraw); + resize.observe(canvas); + const onWheel = (event: WheelEvent): void => { + event.preventDefault(); + fitFrames = 0; + camera.zoom = Math.max(0.05, Math.min(5, camera.zoom * Math.exp(-event.deltaY * 0.001))); + redraw(); + }; + const onPointerDown = (event: PointerEvent): void => { + pointer = { x: event.clientX, y: event.clientY }; + canvas.setPointerCapture(event.pointerId); + fitFrames = 0; + }; + const onPointerMove = (event: PointerEvent): void => { + if (!pointer) return; + camera.centerX -= (event.clientX - pointer.x) / camera.zoom; + camera.centerY -= (event.clientY - pointer.y) / camera.zoom; + pointer = { x: event.clientX, y: event.clientY }; + redraw(); + }; + const onPointerUp = (): void => { pointer = undefined; }; + canvas.addEventListener('wheel', onWheel, { passive: false }); + canvas.addEventListener('pointerdown', onPointerDown); + canvas.addEventListener('pointermove', onPointerMove); + canvas.addEventListener('pointerup', onPointerUp); + canvas.addEventListener('pointercancel', onPointerUp); + + void WebGpuGraphRenderer.create(canvas, { + onDeviceLost: message => setRendererError(`WebGPU device lost: ${message}`), + onFrameComplete: () => { if (continueAfterFrame) schedule(); }, + onFrameRejected: () => { if (continueAfterFrame) schedule(); }, + onRendererError: setRendererError, + }) + .then((created) => { + if (!active) { + created?.dispose(); + return; + } + renderer = created; + if (!renderer) setRendererError('WebGPU is unavailable in this macOS webview.'); + else schedule(); + }) + .catch(error => setRendererError(error instanceof Error ? error.message : String(error))); + + return () => { + active = false; + redrawRef.current = undefined; + if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); + resize.disconnect(); + canvas.removeEventListener('wheel', onWheel); + canvas.removeEventListener('pointerdown', onPointerDown); + canvas.removeEventListener('pointermove', onPointerMove); + canvas.removeEventListener('pointerup', onPointerUp); + canvas.removeEventListener('pointercancel', onPointerUp); + renderer?.dispose(); + layout.pause(); + }; + }, [graph, physicsReady]); + + return ( +
+
+ + {rendererError ?
{rendererError}
: null} +
+ Files + Symbols +
+
+
+
+ {selectedId ? 'Relationships' : 'Graph summary'} + {graph.nodes.length} Nodes · {graph.edges.length} Relationships +
+ {selectedId ? ( + <> + {selectedId} +
+ {relationships.length === 0 ? No visible Relationships : relationships.map(edge => { + const outgoing = edge.from === selectedId; + const target = outgoing ? edge.to : edge.from; + const targetNode = graph.nodes.find(node => node.id === target); + const targetFile = targetNode?.symbol?.filePath ?? target; + return ( + + ); + })} +
+ + ) :

Choose a File to inspect its incoming and outgoing Relationships.

} +
+
+ ); +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx new file mode 100644 index 000000000..f7c36913d --- /dev/null +++ b/apps/desktop/src/main.tsx @@ -0,0 +1,7 @@ +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './styles.css'; + +const root = document.getElementById('root'); +if (!root) throw new Error('CodeGraphy desktop root is missing.'); +createRoot(root).render(); diff --git a/apps/desktop/src/model.test.ts b/apps/desktop/src/model.test.ts new file mode 100644 index 000000000..ce78dca52 --- /dev/null +++ b/apps/desktop/src/model.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { buildFileTree, parseWorkspaceGraphResult } from './model'; + +const graphResult = { + kind: 'ready', + workspaceRoot: '/workspace', + graphCache: '.codegraphy/graph.sqlite', + cacheStatus: { state: 'fresh', staleReasons: [] }, + graph: { + nodes: [ + { id: 'README.md', label: 'README.md' }, + { id: 'src/index.ts', label: 'index.ts' }, + { id: 'src/model/value.ts', label: 'value.ts' }, + { + id: 'src/model/value.ts#value', + label: 'value', + nodeType: 'variable', + symbol: { id: 'value', name: 'value', kind: 'variable', filePath: 'src/model/value.ts' }, + }, + ], + edges: [ + { id: 'src/index.ts->src/model/value.ts#import', from: 'src/index.ts', to: 'src/model/value.ts', kind: 'import' }, + ], + }, +} satisfies unknown; + +describe('desktop workspace graph model', () => { + it('parses Core data and builds a File hierarchy without treating Symbols as Files', () => { + const result = parseWorkspaceGraphResult(graphResult); + + expect(result.kind).toBe('ready'); + if (result.kind !== 'ready') return; + expect(buildFileTree(result.graph)).toEqual([ + { kind: 'file', name: 'README.md', path: 'README.md' }, + { + kind: 'folder', + name: 'src', + path: 'src', + children: [ + { kind: 'file', name: 'index.ts', path: 'src/index.ts' }, + { + kind: 'folder', + name: 'model', + path: 'src/model', + children: [ + { kind: 'file', name: 'value.ts', path: 'src/model/value.ts' }, + ], + }, + ], + }, + ]); + }); + + it('rejects malformed graph Relationships at the process boundary', () => { + const malformed: unknown = { + ...graphResult, + graph: { + ...graphResult.graph, + edges: [{ id: 'broken', from: 'src/index.ts', kind: 'import' }], + }, + }; + + expect(() => parseWorkspaceGraphResult(malformed)).toThrow('invalid Relationship Graph'); + }); +}); diff --git a/apps/desktop/src/model.ts b/apps/desktop/src/model.ts new file mode 100644 index 000000000..d27f01122 --- /dev/null +++ b/apps/desktop/src/model.ts @@ -0,0 +1,195 @@ +export interface DesktopGraphNode { + id: string; + label: string; + nodeType?: string; + symbol?: { + id: string; + name: string; + kind: string; + filePath: string; + }; +} + +export interface DesktopGraphEdge { + id: string; + from: string; + to: string; + kind: string; +} + +export interface DesktopGraph { + nodes: DesktopGraphNode[]; + edges: DesktopGraphEdge[]; +} + +interface ReadyWorkspaceGraphResult { + kind: 'ready'; + workspaceRoot: string; + graphCache: string; + cacheStatus: { + state: 'fresh' | 'stale'; + staleReasons: string[]; + }; + graph: DesktopGraph; + indexing?: { + mode: 'full' | 'incremental'; + analyzedFiles: number; + deletedFiles: number; + reusedFiles: number; + }; + discovery?: { + indexedFiles: number; + totalFound: number; + limitReached: boolean; + }; +} + +export type WorkspaceGraphResult = + | { + kind: 'unreadable'; + workspaceRoot: string; + graphCache: string; + message: string; + } + | ReadyWorkspaceGraphResult; + +export type FileTreeEntry = + | { kind: 'file'; name: string; path: string } + | { kind: 'folder'; name: string; path: string; children: FileTreeEntry[] }; + +interface MutableFolder { + folders: Map; + files: Map; + path: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every(item => typeof item === 'string'); +} + +function isDesktopGraphNode(value: unknown): value is DesktopGraphNode { + if (!isRecord(value) || typeof value.id !== 'string' || typeof value.label !== 'string') return false; + if (value.nodeType !== undefined && typeof value.nodeType !== 'string') return false; + if (value.symbol === undefined) return true; + return isRecord(value.symbol) + && typeof value.symbol.id === 'string' + && typeof value.symbol.name === 'string' + && typeof value.symbol.kind === 'string' + && typeof value.symbol.filePath === 'string'; +} + +function isDesktopGraphEdge(value: unknown): value is DesktopGraphEdge { + return isRecord(value) + && typeof value.id === 'string' + && typeof value.from === 'string' + && typeof value.to === 'string' + && typeof value.kind === 'string'; +} + +function isDesktopGraph(value: unknown): value is DesktopGraph { + return isRecord(value) + && Array.isArray(value.nodes) + && value.nodes.every(isDesktopGraphNode) + && Array.isArray(value.edges) + && value.edges.every(isDesktopGraphEdge); +} + +function isCacheStatus(value: unknown): value is ReadyWorkspaceGraphResult['cacheStatus'] { + return isRecord(value) + && (value.state === 'fresh' || value.state === 'stale') + && isStringArray(value.staleReasons); +} + +function isIndexing(value: unknown): value is NonNullable { + return isRecord(value) + && (value.mode === 'full' || value.mode === 'incremental') + && typeof value.analyzedFiles === 'number' + && typeof value.deletedFiles === 'number' + && typeof value.reusedFiles === 'number'; +} + +function isDiscovery(value: unknown): value is NonNullable { + return isRecord(value) + && typeof value.indexedFiles === 'number' + && typeof value.totalFound === 'number' + && typeof value.limitReached === 'boolean'; +} + +export function parseWorkspaceGraphResult(value: unknown): WorkspaceGraphResult { + if (!isRecord(value) + || typeof value.workspaceRoot !== 'string' + || typeof value.graphCache !== 'string') { + throw new Error('Core returned an invalid Relationship Graph response.'); + } + if (value.kind === 'unreadable' && typeof value.message === 'string') { + return { + kind: 'unreadable', + workspaceRoot: value.workspaceRoot, + graphCache: value.graphCache, + message: value.message, + }; + } + if (value.kind !== 'ready' + || !isDesktopGraph(value.graph) + || !isCacheStatus(value.cacheStatus) + || (value.indexing !== undefined && !isIndexing(value.indexing)) + || (value.discovery !== undefined && !isDiscovery(value.discovery))) { + throw new Error('Core returned an invalid Relationship Graph response.'); + } + return { + kind: 'ready', + workspaceRoot: value.workspaceRoot, + graphCache: value.graphCache, + cacheStatus: value.cacheStatus, + graph: value.graph, + ...(value.indexing ? { indexing: value.indexing } : {}), + ...(value.discovery ? { discovery: value.discovery } : {}), + }; +} + +function createMutableFolder(path: string): MutableFolder { + return { folders: new Map(), files: new Map(), path }; +} + +function compareTreeEntries(left: FileTreeEntry, right: FileTreeEntry): number { + return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' }); +} + +function projectFolder(folder: MutableFolder): FileTreeEntry[] { + const files: FileTreeEntry[] = [...folder.files] + .map(([name, path]) => ({ kind: 'file', name, path })); + const folders: FileTreeEntry[] = [...folder.folders] + .map(([name, child]) => ({ + kind: 'folder', + name, + path: child.path, + children: projectFolder(child), + })); + return [...files, ...folders].sort(compareTreeEntries); +} + +export function buildFileTree(graph: DesktopGraph): FileTreeEntry[] { + const root = createMutableFolder(''); + for (const node of graph.nodes) { + if (node.symbol || (node.nodeType !== undefined && node.nodeType !== 'file')) continue; + const parts = node.id.split('/').filter(Boolean); + const fileName = parts.pop(); + if (!fileName) continue; + let folder = root; + for (const folderName of parts) { + const folderPath = folder.path ? `${folder.path}/${folderName}` : folderName; + let child = folder.folders.get(folderName); + if (!child) { + child = createMutableFolder(folderPath); + folder.folders.set(folderName, child); + } + folder = child; + } + folder.files.set(fileName, node.id); + } + return projectFolder(root); +} diff --git a/apps/desktop/src/packaging.test.ts b/apps/desktop/src/packaging.test.ts new file mode 100644 index 000000000..4d51ad8ce --- /dev/null +++ b/apps/desktop/src/packaging.test.ts @@ -0,0 +1,23 @@ +import { readFile } from 'node:fs/promises'; +import { describe, expect, it } from 'vitest'; + +interface TauriConfiguration { + app?: { security?: { csp?: string } }; + bundle?: { resources?: unknown }; +} + +async function readTauriConfiguration(): Promise { + return JSON.parse(await readFile('src-tauri/tauri.conf.json', 'utf8')) as TauriConfiguration; +} + +describe('desktop packaging contracts', () => { + it('allows the webview to fetch its own renderer WASM asset', async () => { + const configuration = await readTauriConfiguration(); + expect(configuration.app?.security?.csp).toContain("connect-src 'self'"); + }); + + it('preserves the deployed Core runtime hierarchy', async () => { + const configuration = await readTauriConfiguration(); + expect(configuration.bundle?.resources).toEqual(['runtime/']); + }); +}); diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css new file mode 100644 index 000000000..4400791ca --- /dev/null +++ b/apps/desktop/src/styles.css @@ -0,0 +1,147 @@ +:root { + color-scheme: dark; + --bg: #071116; + --panel: #0b171d; + --panel-raised: #0f1d24; + --line: rgba(146, 183, 192, 0.14); + --line-strong: rgba(146, 183, 192, 0.24); + --text: #dce9ed; + --muted: #78919a; + --quiet: #506971; + --accent: #63ddbd; + --accent-soft: rgba(99, 221, 189, 0.12); + --font-ui: Inter, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif; + --font-code: "SFMono-Regular", "SF Mono", Menlo, Consolas, monospace; + font-family: var(--font-ui); + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { box-sizing: border-box; } +html, body, #root { width: 100%; height: 100%; margin: 0; overflow: hidden; } +body { background: var(--bg); color: var(--text); } +button { color: inherit; font: inherit; } + +.desktop-shell { + display: grid; + grid-template-rows: 50px minmax(0, 1fr) 27px; + width: 100%; + height: 100%; + background: + radial-gradient(circle at 78% 12%, rgba(22, 111, 116, .08), transparent 30%), + var(--bg); +} + +.app-toolbar { + display: flex; + align-items: center; + border-bottom: 1px solid var(--line); + background: rgba(8, 19, 24, .92); + user-select: none; +} +.window-drag-space { width: 76px; align-self: stretch; flex: 0 0 auto; } +.brand-lockup { display: flex; align-items: center; min-width: 0; gap: 7px; font-size: 12px; } +.brand-symbol { color: var(--accent); font-size: 24px; line-height: 1; transform: rotate(-10deg); } +.brand-lockup strong { font-size: 12px; letter-spacing: .02em; } +.workspace-name { overflow: hidden; max-width: 300px; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } +.toolbar-actions { display: flex; gap: 6px; margin-left: auto; padding-right: 10px; } +.toolbar-button, .save-button { + min-height: 30px; + border: 1px solid var(--line); + border-radius: 7px; + background: rgba(255,255,255,.025); + color: #9cb2ba; + padding: 0 11px; + font-size: 11px; + cursor: default; +} +.toolbar-button:hover:not(:disabled), .save-button:hover:not(:disabled) { border-color: var(--line-strong); background: rgba(255,255,255,.055); color: var(--text); } +.toolbar-button.is-active { border-color: rgba(99,221,189,.34); background: var(--accent-soft); color: var(--accent); } +.toolbar-button:disabled, .save-button:disabled { opacity: .4; } + +.workspace-grid { display: grid; grid-template-columns: minmax(170px, 16%) minmax(360px, 42%) minmax(420px, 42%); min-height: 0; } +.files-pane, .editor-pane, .graph-pane { min-width: 0; min-height: 0; background: var(--panel); } +.files-pane, .editor-pane { border-right: 1px solid var(--line); } +.files-pane, .editor-pane, .graph-pane { display: grid; grid-template-rows: 36px minmax(0, 1fr); } +.pane-heading { + display: flex; + align-items: center; + justify-content: space-between; + min-width: 0; + border-bottom: 1px solid var(--line); + padding: 0 11px 0 13px; + color: #91a7af; + font-size: 10px; + font-weight: 650; + letter-spacing: .07em; + text-transform: uppercase; +} +.pane-heading > span:last-child { color: var(--quiet); font-family: var(--font-code); font-size: 9px; font-weight: 500; letter-spacing: 0; text-transform: none; } +.editor-heading > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-transform: none; letter-spacing: 0; font-family: var(--font-code); font-weight: 500; } + +.file-tree { min-height: 0; overflow: auto; padding: 7px 6px 32px; } +.tree-file { display: flex; align-items: center; width: 100%; height: 25px; gap: 7px; border: 0; border-radius: 5px; background: transparent; padding: 0 7px; color: #93a7ae; font-family: var(--font-code); font-size: 10.5px; text-align: left; white-space: nowrap; } +.tree-file:hover { background: rgba(255,255,255,.035); color: #c8d7dc; } +.tree-file.is-selected { background: rgba(64, 142, 143, .2); color: #dcf5ef; } +.file-mark { width: 6px; height: 8px; border: 1px solid #52737c; border-radius: 1px; } +.tree-folder > summary { display: flex; align-items: center; height: 25px; gap: 4px; list-style: none; padding: 0 5px; color: #728a92; font-family: var(--font-code); font-size: 10.5px; cursor: default; } +.tree-folder > summary::-webkit-details-marker { display: none; } +.folder-caret { display: inline-block; color: #49636b; font-size: 15px; transform: rotate(90deg); transition: transform .12s ease; } +.tree-folder:not([open]) .folder-caret { transform: rotate(0deg); } +.tree-children { margin-left: 10px; border-left: 1px solid rgba(137,173,182,.1); padding-left: 3px; } + +.editor-host { min-height: 0; height: 100%; overflow: hidden; } +.editor-host .cm-editor { height: 100%; } +.save-button { min-height: 25px; padding: 0 8px; font-size: 9px; text-transform: none; letter-spacing: 0; } +.save-button.has-changes { border-color: rgba(99,221,189,.4); color: var(--accent); } +.empty-editor { display: grid; place-content: center; justify-items: center; min-height: 0; padding: 40px; text-align: center; } +.empty-glyph { color: #284852; font-family: var(--font-code); font-size: 46px; letter-spacing: -.1em; } +.empty-editor h1 { margin: 18px 0 7px; font-size: 18px; font-weight: 520; } +.empty-editor p { max-width: 330px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.65; } + +.graph-panel { display: grid; grid-template-rows: minmax(0, 1fr) minmax(134px, 26%); min-height: 0; } +.graph-canvas-wrap { position: relative; min-height: 0; overflow: hidden; background: #09151b; } +.graph-canvas { display: block; width: 100%; height: 100%; cursor: grab; } +.graph-canvas:active { cursor: grabbing; } +.graph-error { position: absolute; inset: auto 12px 12px; border: 1px solid rgba(255,121,128,.35); border-radius: 7px; background: rgba(65,20,26,.92); padding: 9px 11px; color: #ffb2b6; font-size: 10px; } +.graph-legend { position: absolute; top: 10px; right: 10px; display: flex; gap: 11px; border: 1px solid var(--line); border-radius: 7px; background: rgba(8,20,25,.78); padding: 6px 8px; color: #68818a; font-size: 9px; backdrop-filter: blur(12px); } +.graph-legend span { display: flex; align-items: center; gap: 5px; } +.graph-legend i { width: 7px; height: 7px; border-radius: 50%; } +.legend-file { background: #197c84; border: 1px solid #a6e7d7; } +.legend-symbol { background: #7758b8; border: 1px solid #bb9cff; transform: rotate(45deg); border-radius: 1px !important; } +.relationship-inspector { min-height: 0; overflow: auto; border-top: 1px solid var(--line); background: #0b171d; padding: 10px 12px 16px; } +.inspector-heading { display: flex; justify-content: space-between; color: #718991; font-size: 9px; text-transform: uppercase; letter-spacing: .07em; } +.inspector-heading span:last-child { color: #3f5b64; font-family: var(--font-code); letter-spacing: 0; text-transform: none; } +.selected-node-title { display: block; overflow: hidden; margin-top: 10px; color: #cfe1e6; font-family: var(--font-code); font-size: 10.5px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } +.relationship-list { display: grid; gap: 2px; margin-top: 7px; } +.relationship-list button { display: grid; grid-template-columns: 72px 14px minmax(0,1fr); align-items: center; min-height: 25px; border: 0; border-radius: 4px; background: transparent; padding: 0 6px; color: #80969d; font-family: var(--font-code); font-size: 9.5px; text-align: left; } +.relationship-list button:hover { background: rgba(255,255,255,.035); color: #cbdadd; } +.relationship-kind { overflow: hidden; color: #5bbba5; text-overflow: ellipsis; } +.relationship-direction { color: #49646c; } +.relationship-list button span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.quiet { color: var(--quiet); font-size: 10px; line-height: 1.5; } + +.welcome-state { display: grid; place-content: center; justify-items: center; padding: 60px; text-align: center; } +.welcome-orbit { display: grid; width: 104px; height: 104px; place-items: center; border: 1px solid rgba(99,221,189,.26); border-radius: 50%; box-shadow: 0 0 80px rgba(33,143,135,.13); } +.welcome-orbit::before { position: absolute; width: 66px; height: 66px; border: 1px dashed rgba(116,170,177,.22); border-radius: 50%; content: ''; } +.welcome-orbit span { color: var(--accent); font-size: 42px; } +.eyebrow { margin: 26px 0 14px !important; color: #528c87 !important; font-family: var(--font-code); font-size: 9px !important; letter-spacing: .17em; } +.welcome-state h1 { margin: 0; font-size: clamp(38px, 5vw, 64px); font-weight: 520; line-height: 1.02; letter-spacing: -.045em; } +.welcome-state h1 em { color: #72d6c0; font-family: Georgia, serif; font-weight: 400; } +.welcome-state > p { max-width: 540px; margin: 21px 0 0; color: var(--muted); font-size: 13px; line-height: 1.7; } +.welcome-state > button { margin-top: 27px; min-height: 40px; border: 1px solid rgba(99,221,189,.46); border-radius: 8px; background: #1d705f; padding: 0 18px; color: #edfffa; font-size: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.22); } +.welcome-state > button:hover { background: #25836f; } + +.status-bar { display: flex; align-items: center; gap: 7px; border-top: 1px solid var(--line); background: #071116; padding: 0 9px; color: #557079; font-family: var(--font-code); font-size: 9px; } +.status-dot { width: 5px; height: 5px; border-radius: 50%; background: #4d6f75; } +.status-ready .status-dot { background: var(--accent); box-shadow: 0 0 8px rgba(99,221,189,.5); } +.status-busy .status-dot { background: #f1c267; animation: pulse 1s ease-in-out infinite; } +.status-error { color: #cf777d; } +.status-error .status-dot { background: #e65f68; } +.status-local { margin-left: auto; color: #365159; } +@keyframes pulse { 50% { opacity: .35; } } + +@media (max-width: 1180px) { + .workspace-grid { grid-template-columns: 180px minmax(330px, 1fr) minmax(380px, 1fr); } + .workspace-name { max-width: 150px; } +} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 000000000..13f0d3cc6 --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", "vite.config.ts", "vitest.config.ts"] +} diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts new file mode 100644 index 000000000..304f6325c --- /dev/null +++ b/apps/desktop/vite.config.ts @@ -0,0 +1,16 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + build: { + assetsInlineLimit: 0, + target: 'safari26', + }, + clearScreen: false, + plugins: [react()], + server: { + port: 1420, + strictPort: true, + }, +}); diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts new file mode 100644 index 000000000..6700f15c0 --- /dev/null +++ b/apps/desktop/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + include: ['src/**/*.test.{ts,tsx}'], + }, +}); diff --git a/docs/research/desktop-app-stack.md b/docs/research/desktop-app-stack.md new file mode 100644 index 000000000..bfcecdc25 --- /dev/null +++ b/docs/research/desktop-app-stack.md @@ -0,0 +1,100 @@ +# macOS desktop app stack research + +Research completed: 2026-08-06. Sources were checked on that date. This note records the stack decision for Trello #275; it does not redefine the product model in [`CONTEXT.md`](../../CONTEXT.md) or the principles in [`docs/PHILOSOPHY.md`](../PHILOSOPHY.md). + +## Decision + +Build the macOS MVP as a **Tauri 2 app with a Rust shell, the existing React/Vite web stack, CodeMirror 6, the existing WebGPU/WASM graph renderer, and a bundled Core service process**. + +Set **macOS 26.0 as the minimum system version**. This is a product constraint, not a build default: Tauri uses the operating system's `WKWebView`, and WebKit shipped WebGPU in Safari 26 on macOS 26. CodeGraphy's graph renderer has no non-WebGPU implementation. Supporting macOS 25 or earlier would therefore require either a second renderer or a bundled Chromium runtime. Both conflict with the smallest long-term architecture for this MVP. Tauri exposes `bundle.macOS.minimumSystemVersion` for enforcing this requirement in the app bundle ([Tauri webview versions](https://v2.tauri.app/reference/webview-versions/), updated 2026; [WebKit, “News from WWDC25,” June 9, 2025](https://webkit.org/blog/16993/news-from-wwdc25-web-technology-coming-this-fall-in-safari-26-beta/); [Tauri macOS app bundle](https://v2.tauri.app/distribute/macos-application-bundle/)). + +Ship native Apple Silicon and Intel artifacts only after each architecture passes the same runtime probe. Apple Silicon is the first required artifact. Do not label an Intel artifact supported until `navigator.gpu`, adapter creation, the CodeGraphy render pipeline, the bundled native Core modules, signing, notarization, and an installed-app launch all pass on Intel hardware. + +## Architecture + +```text +CodeGraphy.app +├── Tauri 2 Rust shell +│ ├── macOS window, menu, open dialog, and lifecycle +│ ├── narrow, validated file read/save commands +│ └── owns and monitors the Core child process +├── WKWebView frontend +│ ├── thin File and Folder hierarchy +│ ├── CodeMirror 6 file viewer/editor +│ └── existing @codegraphy-dev/graph-renderer WebGPU/WASM Graph View +└── bundled Core service process + ├── existing @codegraphy-dev/core and native dependencies + ├── File Discovery, Tree-sitter Analysis, and Indexing + ├── workspace-local .codegraphy/graph.sqlite Graph Cache + └── request/response plus lifecycle events over JSON Lines stdio +``` + +The ownership rules are: + +- **Core remains the only graph engine.** The desktop app must call Core for File Discovery, Indexing, Graph Cache reads and writes, Nodes, Symbols, Relationships, Graph Scope, Filters, and plugin analysis. Rust must not recreate these systems. +- **Rust owns host integration, not product analysis.** It opens native dialogs, validates workspace-relative paths, performs safe file reads and writes, supervises the child process, and translates a small typed command/event protocol for the webview. +- **The webview owns presentation.** It renders the hierarchy, editor, and Graph View. It does not get unrestricted filesystem or shell access. +- **Use one long-running Core process for the active CodeGraphy Workspace.** Communicate through newline-delimited JSON on stdin/stdout with request IDs and explicit lifecycle events. Stderr is diagnostic output. This avoids a localhost port, an authentication scheme, duplicate CLI launches, and repeated loading of Tree-sitter and SQLite native modules. +- **Bundle a matching Node runtime and the built Core package as a Tauri sidecar resource for each CPU architecture.** Tauri explicitly supports external binaries with target-triple names and process control ([Tauri external binaries](https://v2.tauri.app/develop/sidecar/)). Do not require a user's Node or pnpm installation. Do not make Node's single-executable application feature the release foundation yet: Node 26 still marks it as active development and its current CI statement only tests macOS arm64 ([Node.js single executable applications, v26.5.0](https://nodejs.org/api/single-executable-applications.html)). A conventional bundled runtime is larger but is the reliable path for Core's native Tree-sitter and `libsql` dependencies. +- **Keep workspace state user-owned.** The Graph Cache and workspace settings remain in `.codegraphy/`; app-global UI state uses the normal macOS Application Support location. Do not copy source files or graph data into an account or cloud service. + +The existing repository supports this split. `@codegraphy-dev/core` already exports a headless API and the `codegraphy` CLI, owns `.codegraphy/graph.sqlite`, and depends on native Tree-sitter grammars and `libsql`. `@codegraphy-dev/graph-renderer` already owns WebGPU drawing and WASM physics. React and Vite are already used by CodeGraphy interfaces. The desktop package should compose these boundaries instead of moving their behavior into Rust. + +## Why Tauri 2 + +Tauri 2 has the exact responsibilities needed here: a Rust application process, the system webview, scoped frontend-to-Rust commands, sidecar bundling, macOS bundles, DMGs, signing, notarization, an updater, and a maintained GitHub release action. Its documented process model uses `WKWebView` on macOS ([Tauri process model](https://v2.tauri.app/concept/process-model/)). Its sidecar mechanism accepts executables written in any language and bundles per-target binaries ([Tauri external binaries](https://v2.tauri.app/develop/sidecar/)). + +The realistic alternatives are worse for this product now: + +| Option | Benefit | Reason not selected | +|---|---|---| +| Electron | Bundled Chromium gives a controlled WebGPU version on older macOS releases, and its Node main/utility processes could load Core directly. | It does not meet the Rust-shell direction and bundles Chromium and Node into a larger multi-process runtime. Electron's own documentation says it embeds both Chromium and Node.js ([Electron introduction](https://www.electronjs.org/docs/latest/)); its process model adds a Node main process and Chromium renderer processes ([Electron process model](https://www.electronjs.org/docs/latest/tutorial/process-model)). Choose it only if supporting pre-macOS-26 systems becomes more important than the thin Rust shell. | +| Raw `wry`/`tao` | Maximum control with the same Rust and `WKWebView` foundation. | `wry` is the webview library underneath this class of app, not the complete security, command, bundling, updater, and release system ([wry README](https://github.com/tauri-apps/wry)). Rebuilding those layers adds product risk without solving the WebGPU OS floor. | +| SwiftUI/AppKit plus `WKWebView` | Best access to native macOS controls. | It still inherits the same `WKWebView` WebGPU constraint while adding a second UI language and manual Rust/Swift integration. It does not improve reuse of the existing React Graph View. | +| GPUI, Floem, or another native Rust UI | Native GPU rendering and very low overhead are possible. | This path requires replacing both the web UI and the editor integration. Zed's GPUI is custom, pre-1.0, and uses Metal on macOS ([GPUI README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md)). Floem says it is still maturing and can make breaking changes ([Floem README](https://github.com/lapce/floem)). Adopting either would turn the MVP into an editor and renderer rewrite. | + +## Why CodeMirror 6 + +Use **CodeMirror 6**, loaded with only the language and editor extensions required for the open file. The MVP needs fast viewing, basic syntax highlighting, editing, undo/redo, search, and safe save. It does not need to reproduce an IDE language-service host. + +CodeMirror is an extensible web editor with a minimal setup and separately installed language packages ([CodeMirror](https://codemirror.net/); [CodeMirror core extensions](https://codemirror.com/docs/extensions/)). Its current changelog shows active maintenance through June 2026, including accessibility and large-document fixes ([CodeMirror changelog](https://codemirror.com/docs/changelog/)). This fits a focused code navigator where Core, not the editor, already performs Tree-sitter analysis. + +Monaco is a strong engine, but it is the wrong default here. Microsoft describes Monaco as the fully featured VS Code editor, generated from VS Code source with service shims. Smart features run in web workers; models need durable URIs and disposal; VS Code extensions do not run in Monaco ([Monaco README and FAQ](https://github.com/microsoft/monaco-editor)). Those systems are valuable for an IDE, but they duplicate complexity before CodeGraphy needs it. Monaco should be reconsidered only if the product explicitly adds IDE-grade language services that CodeMirror extensions and an LSP client cannot satisfy. + +Obsidian is useful evidence for this choice: its official changelog exposes its CodeMirror 6 extension API and records Electron and CodeMirror upgrades ([Obsidian 0.13.7, December 3, 2021](https://obsidian.md/changelog/page/17/); [Obsidian 1.8.3, January 28, 2025](https://obsidian.md/changelog/page/7/)). CodeGraphy should copy the product lesson—local files, a metadata cache, a focused editor, and a graph—not Obsidian's Electron shell. + +## Lessons from comparable tools + +- **Obsidian: keep Files authoritative and the cache rebuildable.** Obsidian says a vault is a local filesystem folder of plain-text files, external tools can edit them, and its local metadata cache powers Graph and Outline views ([Obsidian storage help](https://obsidian.md/help/Files%2Band%2Bfolders/How%2BObsidian%2Bstores%2Bdata)). This closely matches CodeGraphy's workspace-owned Files and rebuildable Graph Cache. +- **Zed: native performance is real, but owning the whole stack is a different product investment.** Zed says it built a custom Rust GPU UI framework and organized the app around shaders; GPUI uses Metal on macOS ([Zed 1.0, April 29, 2026](https://zed.dev/blog/zed-1-0); [GPUI README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md)). CodeGraphy already has a specialized WebGPU renderer, so it should reuse that advantage without also building a native editor and UI toolkit. +- **Lapce: a Rust-native editor requires dedicated editor architecture.** Lapce is pure Rust, uses Floem, Rope Science, and `wgpu`, and includes LSP and remote-development systems ([Lapce README](https://github.com/lapce/lapce)). That validates the ceiling of native Rust, but also shows the scope that CodeGraphy avoids by adopting CodeMirror. +- **Fleet: keep the product sharply differentiated.** Fleet separated frontend editor behavior, workspace state, and a Rust system daemon in a distributed architecture ([Fleet architecture overview, January 2022](https://blog.jetbrains.com/fleet/2022/01/fleet-below-deck-part-i-architecture-overview/)). JetBrains later ended Fleet distribution because a second general-purpose IDE family overlapped existing products and lacked a clear reason to switch ([The Future of Fleet, December 2025, updated May 2026](https://blog.jetbrains.com/fleet/2025/12/the-future-of-fleet/)). CodeGraphy should remain a fast Relationship Graph navigator with lightweight editing, not expand the MVP into another general-purpose IDE. + +## macOS packaging and release + +The direct-download release path is: + +1. Build architecture-specific app bundles and DMGs on macOS runners. Tauri calls DMG the common direct-download format and produces it with `tauri build --bundles dmg` ([Tauri DMG guide](https://v2.tauri.app/distribute/dmg/)). +2. Sign the app, bundled Node executable, native `.node` modules, and other nested executable code with a **Developer ID Application** certificate and Hardened Runtime. Apple requires distribution signing before direct distribution; its guidance requires a valid signature, secure timestamp, and Hardened Runtime for notarization ([Apple distribution signing](https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac); [Apple notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)). +3. Notarize the final deliverable with `notarytool` through Tauri's supported App Store Connect API-key or Apple-ID environment variables, staple the ticket, and inspect the notary log. Tauri documents these credential hooks and states that notarization is required with Developer ID Application signing ([Tauri macOS signing, updated May 17, 2026](https://v2.tauri.app/distribute/sign/macos/)). +4. Verify the installed artifact, not only the build directory: mount the DMG, copy the app to `/Applications`, launch it, run `codesign --verify --deep --strict --verbose=2`, run `spctl --assess --type execute --verbose=4`, confirm the stapled ticket, open a real workspace, save a File, index it, and render its Relationship Graph. +5. Upload only verified artifacts to a draft GitHub Release. Tauri's maintained action builds targets, can create a draft release, and uploads artifacts; its documented matrix includes separate Apple Silicon and Intel builds ([Tauri GitHub pipeline](https://v2.tauri.app/distribute/pipelines/github/); [tauri-action](https://github.com/tauri-apps/tauri-action)). The repository's release workflow should own the final trigger and version source. +6. Enable the Tauri updater only after the first signed release path works. The updater requires cryptographic signatures and does not allow signature verification to be disabled. It can consume `latest.json` from GitHub Releases, and `tauri-action` can generate that file ([Tauri updater](https://v2.tauri.app/plugin/updater/)). Keep the updater private key separate from the Apple signing identity and store both only in protected release secrets. + +An unsigned or ad-hoc-signed DMG is a development artifact, not a production download. Apple tightened Gatekeeper behavior in macOS Sequoia, and Apple recommends notarization for software distributed outside the Mac App Store ([Apple developer news, August 6, 2024](https://developer.apple.com/news/?id=saqachfa)). The website download control must remain visibly gated until a signed, notarized, stapled, installed, and runtime-tested GitHub Release asset exists. + +## Required proof before release + +- A startup probe records the macOS and WebKit versions, `navigator.gpu` availability, adapter/device creation, and the first successful renderer frame. +- The bundled Core process reports its Core and Node versions and successfully loads every native Tree-sitter grammar plus `libsql` on the release architecture. +- Killing the window or changing workspaces terminates the old Core process. A crash produces a visible recoverable error and diagnostic output rather than an orphan process. +- File save uses a workspace-relative validated path and detects an external modification before overwrite. A successful save triggers bounded Core Indexing and the same Core-owned Relationship Graph appears again. +- Signing verification covers nested binaries. Notarization, stapling, Gatekeeper assessment, DMG install, updater signature metadata, and the complete three-pane runtime path all run against the final release artifact. + +## Reconsideration triggers + +Revisit this decision only when a product requirement changes: + +- If CodeGraphy must support macOS 25 or earlier while retaining the current WebGPU-only renderer, switch the shell decision to Electron or fund a separately scoped embedded-Chromium/native-renderer investigation. +- If CodeGraphy becomes a general-purpose IDE with deep language-service behavior, reevaluate Monaco or a native editor architecture. +- If Core gains a stable native executable that fully owns current Node/Core behavior, replace the bundled Node service without changing the stdio protocol or UI ownership boundaries. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 41ab7558c..e8b26d4df 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -444,6 +444,12 @@ export type { WorkspaceStatusResult, } from './workspace/requestTypes'; export { requestCodeGraphyIndexWorkspace } from './workspace/requestIndexing'; +export type { + WorkspaceGraphDependencies, + WorkspaceGraphInput, + WorkspaceGraphResult, +} from './workspace/requestGraph'; +export { requestCodeGraphyWorkspaceGraph } from './workspace/requestGraph'; export { requestWorkspaceGraphQuery } from './workspace/requestQuery'; export { readCodeGraphyWorkspaceStatusForCli } from './workspace/requestStatus'; export type { diff --git a/packages/core/src/workspace/requestGraph.ts b/packages/core/src/workspace/requestGraph.ts new file mode 100644 index 000000000..b983ec998 --- /dev/null +++ b/packages/core/src/workspace/requestGraph.ts @@ -0,0 +1,99 @@ +import * as path from 'node:path'; +import { inspectWorkspaceAnalysisDatabase } from '../graphCache/database/storage'; +import type { IGraphData } from '../graph/contracts'; +import { + readCodeGraphyInstalledPluginCache, + type CodeGraphyInstalledPluginCache, +} from '../plugins/installedCache'; +import { deriveVisibleGraph } from '../visibleGraph'; +import { readWorkspaceQueryGraph } from './queryGraph'; +import { resolveCodeGraphyWorkspacePath } from './requestPaths'; +import type { WorkspaceGraphQueryProjection, WorkspacePathInput } from './requestTypes'; +import { readCodeGraphyWorkspaceStatus } from './status'; + +export interface WorkspaceGraphInput extends WorkspacePathInput { + projection?: WorkspaceGraphQueryProjection; +} + +export type WorkspaceGraphResult = + | { + kind: 'missing'; + workspaceRoot: string; + graphCache: string; + } + | { + kind: 'unreadable'; + workspaceRoot: string; + graphCache: string; + message: string; + } + | { + kind: 'ready'; + workspaceRoot: string; + graphCache: string; + cacheStatus: { + state: 'fresh' | 'stale'; + staleReasons: string[]; + }; + graph: IGraphData; + }; + +export interface WorkspaceGraphDependencies { + cwd(): string; + readInstalledPluginCache(): CodeGraphyInstalledPluginCache; +} + +const DEFAULT_DEPENDENCIES: WorkspaceGraphDependencies = { + cwd: () => process.cwd(), + readInstalledPluginCache: () => readCodeGraphyInstalledPluginCache(), +}; + +/** + * Loads the saved, Core-shaped Relationship Graph without running Indexing. + */ +export function requestCodeGraphyWorkspaceGraph( + input: WorkspaceGraphInput, + dependencies: WorkspaceGraphDependencies = DEFAULT_DEPENDENCIES, +): WorkspaceGraphResult { + const workspaceRoot = resolveCodeGraphyWorkspacePath(input.workspacePath, dependencies.cwd()); + const status = readCodeGraphyWorkspaceStatus(workspaceRoot); + const graphCache = path.relative(workspaceRoot, status.graphCachePath); + if (!status.hasGraphCache) { + return { kind: 'missing', workspaceRoot, graphCache }; + } + + const inspection = inspectWorkspaceAnalysisDatabase(workspaceRoot); + if (!inspection.ok) { + return { + kind: 'unreadable', + workspaceRoot, + graphCache, + message: inspection.message ?? 'The Graph Cache could not be read safely.', + }; + } + + const projected = readWorkspaceQueryGraph( + workspaceRoot, + dependencies.readInstalledPluginCache(), + input.projection, + ); + const graph = deriveVisibleGraph(projected.graphData, { + scope: { + nodes: Object.entries(projected.scope.nodes).map(([type, enabled]) => ({ type, enabled })), + edges: Object.entries(projected.scope.edges).map(([type, enabled]) => ({ type, enabled })), + nodeTypes: projected.nodeTypes, + }, + showOrphans: true, + }).graphData; + + return { + kind: 'ready', + workspaceRoot, + graphCache, + cacheStatus: { + state: status.state === 'stale' ? 'stale' : 'fresh', + staleReasons: status.staleReasons, + }, + graph, + }; +} diff --git a/packages/core/tests/workspace/requestGraph.test.ts b/packages/core/tests/workspace/requestGraph.test.ts new file mode 100644 index 000000000..1c2f6ac29 --- /dev/null +++ b/packages/core/tests/workspace/requestGraph.test.ts @@ -0,0 +1,50 @@ +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { requestCodeGraphyIndexWorkspace } from '../../src/workspace/requestIndexing'; +import { requestCodeGraphyWorkspaceGraph } from '../../src/workspace/requestGraph'; +import { + readCodeGraphyWorkspaceSettingsOrInitial, + writeCodeGraphyWorkspaceSettings, +} from '../../src/workspace/settings'; + +describe('workspace/requestGraph', () => { + it('loads the saved Core Graph Scope and Filters without running Indexing', async () => { + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codegraphy-workspace-graph-')); + await fs.writeFile( + path.join(workspaceRoot, 'entry.ts'), + "import { modelValue } from './model';\nexport const entryValue = modelValue;\n", + ); + await fs.writeFile(path.join(workspaceRoot, 'model.ts'), 'export const modelValue = 1;\n'); + await requestCodeGraphyIndexWorkspace({ workspacePath: workspaceRoot }); + + const symbolProjection = requestCodeGraphyWorkspaceGraph({ + workspacePath: workspaceRoot, + projection: { nodeTypes: ['file', 'symbol'] }, + }); + expect(symbolProjection.kind).toBe('ready'); + if (symbolProjection.kind === 'ready') { + expect(symbolProjection.graph.nodes.some(node => node.symbol?.filePath === 'model.ts')).toBe(true); + } + + const settings = readCodeGraphyWorkspaceSettingsOrInitial(workspaceRoot); + writeCodeGraphyWorkspaceSettings(workspaceRoot, { + ...settings, + filterPatterns: ['model.ts'], + }); + + const result = requestCodeGraphyWorkspaceGraph({ workspacePath: workspaceRoot }); + + expect(result.kind).toBe('ready'); + if (result.kind !== 'ready') return; + expect(result.cacheStatus.state).toBe('fresh'); + expect(result.graph.nodes.map(node => node.id)).toContain('entry.ts'); + expect(result.graph.nodes.map(node => node.id)).not.toEqual(expect.arrayContaining([ + expect.stringContaining('model.ts'), + ])); + expect(result.graph.edges).not.toEqual(expect.arrayContaining([ + expect.objectContaining({ from: 'entry.ts', to: 'model.ts' }), + ])); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 474039eb6..84aafcf65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,70 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + apps/desktop: + dependencies: + '@codegraphy-dev/core': + specifier: workspace:* + version: link:../../packages/core + '@codegraphy-dev/graph-renderer': + specifier: workspace:* + version: link:../../packages/graph-renderer + '@codemirror/commands': + specifier: ^6.10.4 + version: 6.10.4 + '@codemirror/lang-javascript': + specifier: ^6.2.5 + version: 6.2.5 + '@codemirror/language': + specifier: ^6.12.4 + version: 6.12.4 + '@codemirror/search': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/state': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.43.8 + version: 6.43.8 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 + '@tauri-apps/api': + specifier: ^2.11.1 + version: 2.11.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@tauri-apps/cli': + specifier: ^2.11.4 + version: 2.11.4 + '@types/react': + specifier: ^18.2.48 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + jsdom: + specifier: ^25.0.0 + version: 25.0.1 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^6.2.0 + version: 6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + apps/web: dependencies: '@base-ui/react': @@ -1044,6 +1108,30 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.8': + resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -1822,6 +1910,9 @@ packages: '@lezer/highlight@1.2.3': resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} @@ -1876,6 +1967,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + '@material-symbols-svg/react@0.11.0': resolution: {integrity: sha512-dyDhOgYaewA8Z0zR5Dck5m6hInhVcmf7LCSw25NKEI9lkohRne/dST4WgjLs1MEahQVzhiTrqwWB582ofkVMXg==} engines: {node: '>=16.0.0'} @@ -3436,6 +3530,85 @@ packages: '@tailwindcss/postcss@4.3.3': resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.4': + resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.4': + resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} + engines: {node: '>= 10'} + hasBin: true + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -4588,6 +4761,9 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -7004,6 +7180,9 @@ packages: structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -8331,6 +8510,62 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.8': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -8891,6 +9126,12 @@ snapshots: dependencies: '@lezer/common': 1.5.2 + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/lr@1.4.10': dependencies: '@lezer/common': 1.5.2 @@ -8938,6 +9179,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@marijn/find-cluster-break@1.0.3': {} + '@material-symbols-svg/react@0.11.0(react@19.2.7)': dependencies: react: 19.2.7 @@ -10497,6 +10740,55 @@ snapshots: postcss: 8.5.22 tailwindcss: 4.3.3 + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/cli-darwin-arm64@2.11.4': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.4': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + optional: true + + '@tauri-apps/cli@2.11.4': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.4 + '@tauri-apps/cli-darwin-x64': 2.11.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 + '@tauri-apps/cli-linux-arm64-musl': 2.11.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-musl': 2.11.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 + '@tauri-apps/cli-win32-x64-msvc': 2.11.4 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -11137,6 +11429,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.35(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -11175,6 +11479,14 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@20.19.37)(jiti@1.21.7)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -11767,6 +12079,8 @@ snapshots: core-util-is@1.0.3: {} + crelt@1.0.7: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -14530,6 +14844,8 @@ snapshots: dependencies: boundary: 2.0.0 + style-mod@4.1.3: {} + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7): dependencies: client-only: 0.0.1 @@ -15223,7 +15539,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@20.19.37)(jiti@1.21.7)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 From 1a53d94438c85e77d35e827b76cd3f70fa9f8338 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 13:28:35 -0700 Subject: [PATCH 02/25] feat(desktop): harden Core runtime lifecycle --- apps/desktop/scripts/core-sidecar.mjs | 75 ++++++++++++++-- .../scripts/prune-sidecar-runtime.d.mts | 4 + .../desktop/scripts/prune-sidecar-runtime.mjs | 78 +++++++++++++++++ apps/desktop/scripts/stage-sidecar.mjs | 75 +++++++++++++--- apps/desktop/src-tauri/src/lib.rs | 86 +++++++++++++++---- apps/desktop/src/App.tsx | 13 ++- apps/desktop/src/bridge.ts | 1 + apps/desktop/src/core-sidecar.test.ts | 69 +++++++++++++++ .../desktop/src/prune-sidecar-runtime.test.ts | 50 +++++++++++ 9 files changed, 410 insertions(+), 41 deletions(-) create mode 100644 apps/desktop/scripts/prune-sidecar-runtime.d.mts create mode 100644 apps/desktop/scripts/prune-sidecar-runtime.mjs create mode 100644 apps/desktop/src/core-sidecar.test.ts create mode 100644 apps/desktop/src/prune-sidecar-runtime.test.ts diff --git a/apps/desktop/scripts/core-sidecar.mjs b/apps/desktop/scripts/core-sidecar.mjs index 6d3eccce8..0de9b8c6e 100644 --- a/apps/desktop/scripts/core-sidecar.mjs +++ b/apps/desktop/scripts/core-sidecar.mjs @@ -1,9 +1,10 @@ #!/usr/bin/env node import { createInterface } from 'node:readline'; +import { pathToFileURL } from 'node:url'; const coreModuleUrl = process.env.CODEGRAPHY_DESKTOP_CORE_MODULE - ? new URL(`file://${process.env.CODEGRAPHY_DESKTOP_CORE_MODULE}`) + ? pathToFileURL(process.env.CODEGRAPHY_DESKTOP_CORE_MODULE) : new URL('./core/dist/index.js', import.meta.url); const core = await import(coreModuleUrl.href); @@ -16,23 +17,58 @@ function isObject(value) { } function parseRequest(value) { - if (!isObject(value) || value.kind !== 'request' || typeof value.id !== 'number') { - throw new Error('Core request must have kind "request" and a numeric id.'); + if (!isObject(value) || value.kind !== 'request' || !Number.isSafeInteger(value.id) || value.id < 0) { + throw new Error('Core request must have kind "request" and a non-negative safe integer id.'); } - if ((value.method !== 'open' && value.method !== 'index') || !isObject(value.params)) { - throw new Error('Core request method must be "open" or "index".'); + if (!['open', 'index', 'update'].includes(value.method) || !isObject(value.params)) { + throw new Error('Core request method must be "open", "index", or "update".'); } if (typeof value.params.workspaceRoot !== 'string' || value.params.workspaceRoot.length === 0) { throw new Error('Core request requires params.workspaceRoot.'); } + if (value.method === 'update' && ( + typeof value.params.relativePath !== 'string' || value.params.relativePath.length === 0 + )) { + throw new Error('Core update request requires params.relativePath.'); + } return { id: value.id, includeSymbols: value.params.includeSymbols === true, method: value.method, + relativePath: value.params.relativePath, workspaceRoot: value.params.workspaceRoot, }; } +let activeEngine; + +async function disposeActiveEngine() { + if (!activeEngine) return; + const previous = activeEngine; + activeEngine = undefined; + await previous.ready.catch(() => undefined); + previous.engine.dispose(); +} + +function startWorkspaceEngine(workspaceRoot) { + if (activeEngine?.workspaceRoot === workspaceRoot) return activeEngine; + if (activeEngine) throw new Error('The previous Core workspace engine is still active.'); + const engine = core.createCodeGraphyWorkspaceEngine({ workspaceRoot }); + const record = { engine, workspaceRoot, ready: engine.index() }; + activeEngine = record; + return record; +} + +function warmWorkspaceEngine(workspaceRoot) { + const record = startWorkspaceEngine(workspaceRoot); + void record.ready.catch(error => write({ + kind: 'event', + event: 'error', + message: error instanceof Error ? error.message : String(error), + workspaceRoot, + })); +} + function graphRequest(request) { return { workspacePath: request.workspaceRoot, @@ -60,9 +96,31 @@ function indexResult(result, graph) { async function runRequest(request) { if (request.method === 'open') { const cached = core.requestCodeGraphyWorkspaceGraph(graphRequest(request)); - if (cached.kind === 'ready' || cached.kind === 'unreadable') return cached; + if (cached.kind === 'ready') { + if (activeEngine?.workspaceRoot !== request.workspaceRoot) { + await disposeActiveEngine(); + warmWorkspaceEngine(request.workspaceRoot); + } + return cached; + } + if (cached.kind === 'unreadable') return cached; + } + + if (request.method === 'update') { + if (activeEngine?.workspaceRoot !== request.workspaceRoot) await disposeActiveEngine(); + const record = startWorkspaceEngine(request.workspaceRoot); + await record.ready; + write({ + kind: 'event', + event: 'indexing', + workspaceRoot: request.workspaceRoot, + filePaths: [request.relativePath], + }); + const result = await record.engine.applyChangedFiles([request.relativePath]); + return indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); } + await disposeActiveEngine(); write({ kind: 'event', event: 'indexing', @@ -71,7 +129,9 @@ async function runRequest(request) { const result = await core.indexCodeGraphyWorkspace({ workspaceRoot: request.workspaceRoot, }); - return indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); + const response = indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); + warmWorkspaceEngine(request.workspaceRoot); + return response; } const lines = createInterface({ input: process.stdin, crlfDelay: Infinity }); @@ -91,3 +151,4 @@ for await (const line of lines) { }); } } +await disposeActiveEngine(); diff --git a/apps/desktop/scripts/prune-sidecar-runtime.d.mts b/apps/desktop/scripts/prune-sidecar-runtime.d.mts new file mode 100644 index 000000000..52edd9f33 --- /dev/null +++ b/apps/desktop/scripts/prune-sidecar-runtime.d.mts @@ -0,0 +1,4 @@ +export declare function pruneDeployedRuntime( + runtimeRoot: string, + target: string, +): { directories: number; files: number }; diff --git a/apps/desktop/scripts/prune-sidecar-runtime.mjs b/apps/desktop/scripts/prune-sidecar-runtime.mjs new file mode 100644 index 000000000..114c537b5 --- /dev/null +++ b/apps/desktop/scripts/prune-sidecar-runtime.mjs @@ -0,0 +1,78 @@ +import { lstatSync, readdirSync, rmSync } from 'node:fs'; +import path from 'node:path'; + +const developmentDirectories = new Set([ + '.github', + 'benchmark', + 'benchmarks', + 'docs', + 'example', + 'examples', + 'scripts', + 'src', + 'test', + 'tests', +]); + +const developmentFileExtensions = [ + '.d.ts', + '.d.ts.map', + '.map', + '.markdown', + '.md', + '.ts', + '.tsx', +]; + +const buildOnlyPackages = [ + 'node-addon-api', + 'npm-check-updates', + 'tree-sitter-cli', +]; + +function isDevelopmentFile(name) { + const lowerName = name.toLowerCase(); + return developmentFileExtensions.some(extension => lowerName.endsWith(extension)); +} + +function pruneDirectory(directory, nativePrebuildDirectory, counters) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isSymbolicLink()) { + if (entry.name === '.bin') { + rmSync(entryPath, { force: true, recursive: true }); + counters.directories += 1; + } + continue; + } + if (entry.isDirectory()) { + const parentName = path.basename(directory); + const removePrebuild = parentName === 'prebuilds' + && !entry.name.startsWith(nativePrebuildDirectory); + if (developmentDirectories.has(entry.name) || entry.name === '.bin' || removePrebuild) { + rmSync(entryPath, { force: true, recursive: true }); + counters.directories += 1; + continue; + } + pruneDirectory(entryPath, nativePrebuildDirectory, counters); + continue; + } + if (entry.isFile() && isDevelopmentFile(entry.name)) { + rmSync(entryPath, { force: true }); + counters.files += 1; + } + } +} + +export function pruneDeployedRuntime(runtimeRoot, target) { + const nativePrebuildDirectory = target.startsWith('aarch64-') ? 'darwin-arm64' : 'darwin-x64'; + const counters = { directories: 0, files: 0 }; + if (!lstatSync(runtimeRoot).isDirectory()) throw new Error(`Runtime is not a directory: ${runtimeRoot}`); + pruneDirectory(runtimeRoot, nativePrebuildDirectory, counters); + for (const packageName of buildOnlyPackages) { + const packagePath = path.join(runtimeRoot, 'node_modules', packageName); + rmSync(packagePath, { force: true, recursive: true }); + counters.directories += 1; + } + return counters; +} diff --git a/apps/desktop/scripts/stage-sidecar.mjs b/apps/desktop/scripts/stage-sidecar.mjs index d32e51436..625cb85d9 100644 --- a/apps/desktop/scripts/stage-sidecar.mjs +++ b/apps/desktop/scripts/stage-sidecar.mjs @@ -1,15 +1,19 @@ import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, + readFileSync, rmSync, + writeFileSync, } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pruneDeployedRuntime } from './prune-sidecar-runtime.mjs'; const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const repoRoot = path.resolve(appRoot, '../..'); @@ -24,6 +28,32 @@ const nodeDistributionArchitecture = { arm64: 'arm64', x64: 'x64', }; +const nodeArchiveChecksums = { + arm64: '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6', + x64: '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026', +}; +const nativeRuntimeModules = [ + '@driftlog/tree-sitter-dart', + '@tree-sitter-grammars/tree-sitter-kotlin', + '@tree-sitter-grammars/tree-sitter-lua/bindings/node/index.js', + 'libsql', + 'tree-sitter', + 'tree-sitter-c', + 'tree-sitter-c-sharp', + 'tree-sitter-cpp', + 'tree-sitter-go', + 'tree-sitter-haskell', + 'tree-sitter-java', + 'tree-sitter-javascript', + 'tree-sitter-objc', + 'tree-sitter-php', + 'tree-sitter-python', + 'tree-sitter-ruby', + 'tree-sitter-rust', + 'tree-sitter-scala', + 'tree-sitter-swift', + 'tree-sitter-typescript', +]; if (process.platform !== 'darwin') { throw new Error('CodeGraphy desktop sidecars must be staged on macOS.'); @@ -41,18 +71,31 @@ const runtimeCache = process.env.CODEGRAPHY_DESKTOP_RUNTIME_CACHE const nodeArchiveName = `node-v${nodeVersion}-darwin-${nodeDistributionArchitecture[process.arch]}.tar.gz`; const nodeDistributionRoot = path.join(runtimeCache, nodeArchiveName.replace(/\.tar\.gz$/u, '')); const nodeExecutable = path.join(nodeDistributionRoot, 'bin', 'node'); -if (!existsSync(nodeExecutable)) { +const expectedArchiveChecksum = nodeArchiveChecksums[process.arch]; +if (!expectedArchiveChecksum) throw new Error(`No Node checksum for architecture: ${process.arch}`); +const checksumMarker = path.join(nodeDistributionRoot, '.codegraphy-checksum'); +const cachedChecksum = existsSync(checksumMarker) ? readFileSync(checksumMarker, 'utf8').trim() : undefined; +if (!existsSync(nodeExecutable) || cachedChecksum !== expectedArchiveChecksum) { mkdirSync(runtimeCache, { recursive: true }); const archivePath = path.join(runtimeCache, nodeArchiveName); - execFileSync('curl', [ - '--fail', - '--location', - '--output', - archivePath, - `https://nodejs.org/dist/v${nodeVersion}/${nodeArchiveName}`, - ], { stdio: 'inherit' }); + if (!existsSync(archivePath)) { + execFileSync('curl', [ + '--fail', + '--location', + '--output', + archivePath, + `https://nodejs.org/dist/v${nodeVersion}/${nodeArchiveName}`, + ], { stdio: 'inherit' }); + } + const archiveChecksum = createHash('sha256').update(readFileSync(archivePath)).digest('hex'); + if (archiveChecksum !== expectedArchiveChecksum) { + throw new Error(`Node archive checksum mismatch for ${nodeArchiveName}.`); + } + rmSync(nodeDistributionRoot, { force: true, recursive: true }); execFileSync('tar', ['-xzf', archivePath, '-C', runtimeCache], { stdio: 'inherit' }); + writeFileSync(checksumMarker, `${expectedArchiveChecksum}\n`); } +execFileSync('codesign', ['--verify', '--strict', nodeExecutable], { stdio: 'inherit' }); execFileSync(nodeExecutable, ['--version'], { stdio: 'inherit' }); execFileSync('pnpm', [ @@ -78,17 +121,23 @@ execFileSync('pnpm', [ // Hoisted deploy can rewrite shared workspace linker metadata. Restore the // checked-in lockfile layout before any repository build or test continues. execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot, stdio: 'inherit' }); +const pruned = pruneDeployedRuntime(path.join(runtimeRoot, 'core'), target); +process.stdout.write(`Pruned ${pruned.directories} development directories and ${pruned.files} development files.\n`); copyFileSync(path.join(appRoot, 'scripts', 'core-sidecar.mjs'), path.join(runtimeRoot, 'sidecar.mjs')); -execFileSync(nodeExecutable, [ - '--input-type=module', - '--eval', - `await import(${JSON.stringify(pathToFileURL(path.join(runtimeRoot, 'core', 'dist', 'index.js')).href)})`, -], { stdio: 'inherit' }); mkdirSync(binaryRoot, { recursive: true }); const sidecarPath = path.join(binaryRoot, `codegraphy-core-${target}`); copyFileSync(nodeExecutable, sidecarPath); chmodSync(sidecarPath, 0o755); +execFileSync('strip', ['-x', sidecarPath], { stdio: 'inherit' }); +execFileSync('codesign', ['--force', '--sign', '-', '--timestamp=none', sidecarPath], { stdio: 'inherit' }); +execFileSync(sidecarPath, ['--version'], { stdio: 'inherit' }); +const coreModuleUrl = pathToFileURL(path.join(runtimeRoot, 'core', 'dist', 'index.js')).href; +const runtimeProbe = `await Promise.all(${JSON.stringify(nativeRuntimeModules)}.map(module => import(module))); await import(${JSON.stringify(coreModuleUrl)});`; +execFileSync(sidecarPath, ['--input-type=module', '--eval', runtimeProbe], { + cwd: path.join(runtimeRoot, 'core'), + stdio: 'inherit', +}); const licensesSource = path.join(repoRoot, 'LICENSE'); cpSync(licensesSource, path.join(runtimeRoot, 'LICENSE')); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index a2d410494..1f37e55c6 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -34,6 +34,22 @@ struct CoreProcess { events: Receiver, } +enum CoreRequestAction { + Open, + Index, + Update { relative_path: String }, +} + +impl CoreRequestAction { + fn method(&self) -> &'static str { + match self { + Self::Open => "open", + Self::Index => "index", + Self::Update { .. } => "update", + } + } +} + impl Drop for CoreProcess { fn drop(&mut self) { if let Some(child) = self.child.take() { @@ -201,10 +217,31 @@ fn spawn_core_process(app: &AppHandle) -> Result { }) } +fn build_core_request( + request_id: u64, + action: &CoreRequestAction, + workspace_root: &Path, + include_symbols: bool, +) -> Value { + let mut params = json!({ + "workspaceRoot": workspace_root, + "includeSymbols": include_symbols, + }); + if let CoreRequestAction::Update { relative_path } = action { + params["relativePath"] = Value::String(relative_path.clone()); + } + json!({ + "kind": "request", + "id": request_id, + "method": action.method(), + "params": params, + }) +} + async fn request_core( app: &AppHandle, service: &CoreService, - method: &str, + action: &CoreRequestAction, workspace_root: &Path, include_symbols: bool, ) -> Result { @@ -218,15 +255,7 @@ async fn request_core( .process .as_mut() .ok_or_else(|| "Core service is unavailable.".to_string())?; - let request = json!({ - "kind": "request", - "id": request_id, - "method": method, - "params": { - "workspaceRoot": workspace_root, - "includeSymbols": include_symbols, - }, - }); + let request = build_core_request(request_id, action, workspace_root, include_symbols); let mut request_bytes = serde_json::to_vec(&request) .map_err(|error| format!("Unable to encode Core request: {error}"))?; request_bytes.push(b'\n'); @@ -312,6 +341,7 @@ async fn load_workspace_graph( workspace_root: String, reindex: bool, include_symbols: bool, + changed_path: Option, ) -> Result { let root = canonical_workspace_root(Path::new(&workspace_root))?; { @@ -321,14 +351,18 @@ async fn load_workspace_graph( .map_err(|_| "Workspace state is unavailable.".to_string())?; *active_root = Some(root.clone()); } - request_core( - &app, - &core, - if reindex { "index" } else { "open" }, - &root, - include_symbols, - ) - .await + let action = match (reindex, changed_path) { + (true, Some(_)) => { + return Err("A graph request cannot re-index and update one File.".to_string()); + } + (true, None) => CoreRequestAction::Index, + (false, Some(relative_path)) => { + resolve_workspace_file(&root, &relative_path)?; + CoreRequestAction::Update { relative_path } + } + (false, None) => CoreRequestAction::Open, + }; + request_core(&app, &core, &action, &root, include_symbols).await } #[tauri::command] @@ -418,4 +452,20 @@ mod tests { "export const value = 2;\n" ); } + + #[test] + fn update_request_names_the_saved_file() { + let request = build_core_request( + 7, + &CoreRequestAction::Update { + relative_path: "src/index.ts".to_string(), + }, + Path::new("/tmp/example"), + true, + ); + + assert_eq!(request["method"], "update"); + assert_eq!(request["params"]["relativePath"], "src/index.ts"); + assert_eq!(request["params"]["includeSymbols"], true); + } } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 029d72e4d..b24d2eecc 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -39,17 +39,20 @@ export function App(): React.ReactElement { const openGraph = useCallback(async ( root: string, - options: { reindex: boolean; symbols: boolean }, + options: { reindex: boolean; symbols: boolean; changedPath?: string }, ): Promise => { setStatus({ kind: 'busy', - message: options.reindex ? 'Core is re-indexing the workspace…' : 'Loading the Graph Cache…', + message: options.changedPath + ? `Core is updating ${options.changedPath}…` + : options.reindex ? 'Core is re-indexing the workspace…' : 'Loading the Graph Cache…', }); try { const result = await loadWorkspaceGraph({ workspaceRoot: root, reindex: options.reindex, includeSymbols: options.symbols, + changedPath: options.changedPath, }); setWorkspaceRoot(result.workspaceRoot); setGraphResult(result); @@ -110,7 +113,11 @@ export function App(): React.ReactElement { }); setDocument(saved); setDraft(saved.content); - await openGraph(workspaceRoot, { reindex: true, symbols: includeSymbols }); + await openGraph(workspaceRoot, { + reindex: false, + symbols: includeSymbols, + changedPath: saved.path, + }); } catch (error) { setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); } finally { diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts index 5ef695931..a9cd219af 100644 --- a/apps/desktop/src/bridge.ts +++ b/apps/desktop/src/bridge.ts @@ -39,6 +39,7 @@ export async function loadWorkspaceGraph(input: { workspaceRoot: string; reindex: boolean; includeSymbols: boolean; + changedPath?: string; }): Promise { const result = await invoke('load_workspace_graph', input); return parseWorkspaceGraphResult(result); diff --git a/apps/desktop/src/core-sidecar.test.ts b/apps/desktop/src/core-sidecar.test.ts new file mode 100644 index 000000000..35f5debb9 --- /dev/null +++ b/apps/desktop/src/core-sidecar.test.ts @@ -0,0 +1,69 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { force: true, recursive: true }); + } +}); + +describe('Core desktop sidecar', () => { + it('keeps a workspace engine and applies a saved File as an incremental change', () => { + const temporaryDirectory = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-sidecar-test-')); + temporaryDirectories.push(temporaryDirectory); + const logPath = path.join(temporaryDirectory, 'engine.log'); + const coreModulePath = path.join(temporaryDirectory, 'core.mjs'); + writeFileSync(coreModulePath, ` + import { appendFileSync } from 'node:fs'; + const log = value => appendFileSync(${JSON.stringify(logPath)}, value + '\\n'); + const graph = workspaceRoot => ({ + kind: 'ready', + workspaceRoot, + graphCache: '.codegraphy/graph.sqlite', + cacheStatus: { state: 'fresh', staleReasons: [] }, + graph: { nodes: [], edges: [] }, + }); + const result = (workspaceRoot, analyzedFiles, reusedFiles) => ({ + workspaceRoot, + graphCachePath: workspaceRoot + '/.codegraphy/graph.sqlite', + files: [{ relativePath: 'src/index.ts' }], + totalFound: 1, + limitReached: false, + indexing: { mode: 'incremental', analyzedFiles, deletedFiles: 0, reusedFiles }, + }); + export const requestCodeGraphyWorkspaceGraph = input => graph(input.workspacePath); + export const indexCodeGraphyWorkspace = async input => result(input.workspaceRoot, 1, 0); + export const createCodeGraphyWorkspaceEngine = options => ({ + index: async () => { log('index'); return result(options.workspaceRoot, 0, 1); }, + applyChangedFiles: async paths => { log('apply:' + paths.join(',')); return result(options.workspaceRoot, 1, 0); }, + dispose: () => log('dispose'), + }); + `); + const input = [ + { kind: 'request', id: 1, method: 'open', params: { workspaceRoot: temporaryDirectory, includeSymbols: false } }, + { kind: 'request', id: 2, method: 'update', params: { workspaceRoot: temporaryDirectory, relativePath: 'src/index.ts', includeSymbols: false } }, + ].map(request => JSON.stringify(request)).join('\n'); + + const output = execFileSync(process.execPath, ['scripts/core-sidecar.mjs'], { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, CODEGRAPHY_DESKTOP_CORE_MODULE: coreModulePath }, + input: `${input}\n`, + }); + const messages = output.trim().split('\n').map(line => JSON.parse(line) as Record); + const responses = messages.filter(message => message.kind === 'response'); + + expect(responses).toHaveLength(2); + expect(responses[1]).toMatchObject({ + id: 2, + outcome: 'success', + result: { indexing: { mode: 'incremental', analyzedFiles: 1 } }, + }); + expect(readFileSync(logPath, 'utf8')).toBe('index\napply:src/index.ts\ndispose\n'); + }); +}); diff --git a/apps/desktop/src/prune-sidecar-runtime.test.ts b/apps/desktop/src/prune-sidecar-runtime.test.ts new file mode 100644 index 000000000..d334c39db --- /dev/null +++ b/apps/desktop/src/prune-sidecar-runtime.test.ts @@ -0,0 +1,50 @@ +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { pruneDeployedRuntime } from '../scripts/prune-sidecar-runtime.mjs'; + +const temporaryDirectories: string[] = []; + +function temporaryRuntime(): string { + const directory = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-runtime-prune-')); + temporaryDirectories.push(directory); + return directory; +} + +function fixtureFile(root: string, relativePath: string): void { + const target = path.join(root, relativePath); + mkdirSync(path.dirname(target), { recursive: true }); + writeFileSync(target, relativePath); +} + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { force: true, recursive: true }); + } +}); + +describe('pruneDeployedRuntime', () => { + it('keeps runtime code, licenses, and only the target native prebuild', () => { + const runtime = temporaryRuntime(); + fixtureFile(runtime, 'package/index.js'); + fixtureFile(runtime, 'package/LICENSE'); + fixtureFile(runtime, 'package/README.md'); + fixtureFile(runtime, 'package/src/parser.c'); + fixtureFile(runtime, 'package/prebuilds/darwin-arm64/parser.node'); + fixtureFile(runtime, 'package/prebuilds/darwin-x64/parser.node'); + fixtureFile(runtime, 'package/prebuilds/linux-arm64/parser.node'); + fixtureFile(runtime, 'node_modules/tree-sitter-cli/cli.js'); + + pruneDeployedRuntime(runtime, 'aarch64-apple-darwin'); + + expect(existsSync(path.join(runtime, 'package/index.js'))).toBe(true); + expect(existsSync(path.join(runtime, 'package/LICENSE'))).toBe(true); + expect(existsSync(path.join(runtime, 'package/README.md'))).toBe(false); + expect(existsSync(path.join(runtime, 'package/src'))).toBe(false); + expect(existsSync(path.join(runtime, 'package/prebuilds/darwin-arm64/parser.node'))).toBe(true); + expect(existsSync(path.join(runtime, 'package/prebuilds/darwin-x64'))).toBe(false); + expect(existsSync(path.join(runtime, 'package/prebuilds/linux-arm64'))).toBe(false); + expect(existsSync(path.join(runtime, 'node_modules/tree-sitter-cli'))).toBe(false); + }); +}); From 9f4c28984385ceb67f84e31ff6d98c4156805170 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 13:48:50 -0700 Subject: [PATCH 03/25] build(desktop): add verified macOS release path --- .github/workflows/desktop-release.yml | 174 ++++++++++++++++++ apps/desktop/package.json | 2 + apps/desktop/scripts/runtime-contract.d.mts | 3 + apps/desktop/scripts/runtime-contract.mjs | 29 +++ .../desktop/scripts/sign-native-runtime.d.mts | 2 + apps/desktop/scripts/sign-native-runtime.mjs | 25 +++ apps/desktop/scripts/stage-sidecar.mjs | 50 +++-- .../scripts/verify-release-bundle.d.mts | 1 + .../desktop/scripts/verify-release-bundle.mjs | 164 +++++++++++++++++ apps/desktop/src-tauri/Cargo.toml | 3 +- .../src-tauri/Entitlements.ad-hoc.plist | 12 ++ apps/desktop/src-tauri/tauri.ad-hoc.conf.json | 8 + apps/desktop/src/sign-native-runtime.test.ts | 34 ++++ tests/release/desktopPackage.test.mjs | 46 +++++ 14 files changed, 524 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/desktop-release.yml create mode 100644 apps/desktop/scripts/runtime-contract.d.mts create mode 100644 apps/desktop/scripts/runtime-contract.mjs create mode 100644 apps/desktop/scripts/sign-native-runtime.d.mts create mode 100644 apps/desktop/scripts/sign-native-runtime.mjs create mode 100644 apps/desktop/scripts/verify-release-bundle.d.mts create mode 100644 apps/desktop/scripts/verify-release-bundle.mjs create mode 100644 apps/desktop/src-tauri/Entitlements.ad-hoc.plist create mode 100644 apps/desktop/src-tauri/tauri.ad-hoc.conf.json create mode 100644 apps/desktop/src/sign-native-runtime.test.ts create mode 100644 tests/release/desktopPackage.test.mjs diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 000000000..55a26cb6b --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,174 @@ +name: Desktop release + +on: + workflow_dispatch: + inputs: + tag: + description: Exact desktop tag, for example desktop-v0.1.0 + required: true + type: string + +permissions: + contents: write + +concurrency: + group: desktop-release-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + build-verify-draft: + name: Build, verify, and draft Apple Silicon release + runs-on: macos-26 + timeout-minutes: 60 + environment: desktop-release + env: + CODEGRAPHY_DESKTOP_TARGET: aarch64-apple-darwin + CODEGRAPHY_DESKTOP_REQUIRE_RELEASE_SIGNATURE: '1' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Validate release request + env: + RELEASE_TAG: ${{ inputs.tag }} + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + expected_tag="desktop-v${version}" + test "$GITHUB_REF" = "refs/heads/main" + test "$RELEASE_TAG" = "$expected_tag" + test "$(uname -m)" = "arm64" + xcodebuild -version | grep -q '^Xcode 26\.' + + - name: Setup pnpm + uses: pnpm/action-setup@v6.0.8 + with: + version: 10.32.0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.23.2 + cache: pnpm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.96.0 + with: + components: clippy,rustfmt + targets: aarch64-apple-darwin + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test desktop release inputs + run: | + pnpm --filter @codegraphy-dev/desktop test + pnpm --filter @codegraphy-dev/desktop typecheck + cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib + + - name: Import Developer ID certificate + id: signing + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_P8 }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + shell: bash + run: | + set -euo pipefail + keychain="$RUNNER_TEMP/codegraphy-signing.keychain-db" + certificate="$RUNNER_TEMP/codegraphy-developer-id.p12" + api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" + keychain_password="$(openssl rand -hex 24)" + + printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate" + printf '%s' "$APPLE_API_KEY_CONTENT" > "$api_key_path" + chmod 600 "$certificate" "$api_key_path" + + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$certificate" -k "$keychain" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" + security list-keychains -d user -s "$keychain" + + signing_identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)" + test -n "$signing_identity" + printf 'APPLE_SIGNING_IDENTITY=%s\n' "$signing_identity" >> "$GITHUB_ENV" + printf 'APPLE_API_KEY_PATH=%s\n' "$api_key_path" >> "$GITHUB_ENV" + printf 'KEYCHAIN_PATH=%s\n' "$keychain" >> "$GITHUB_ENV" + + - name: Build signed and notarized app + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + run: pnpm --filter @codegraphy-dev/desktop exec tauri build --target "$CODEGRAPHY_DESKTOP_TARGET" --bundles app,dmg + + - name: Sign, notarize, and staple final DMG + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + shell: bash + run: | + set -euo pipefail + bundle_root="apps/desktop/src-tauri/target/${CODEGRAPHY_DESKTOP_TARGET}/release/bundle/dmg" + dmg="$(find "$bundle_root" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + test -n "$dmg" + codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$dmg" + xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" --wait + xcrun stapler staple "$dmg" + + - name: Verify release bundle + run: pnpm --filter @codegraphy-dev/desktop check:bundle + + - name: Prepare release assets + id: assets + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + bundle_root="apps/desktop/src-tauri/target/${CODEGRAPHY_DESKTOP_TARGET}/release/bundle/dmg" + dmg="$(find "$bundle_root" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + test -n "$dmg" + asset="$RUNNER_TEMP/CodeGraphy_${version}_aarch64.dmg" + cp "$dmg" "$asset" + ( + cd "$RUNNER_TEMP" + shasum -a 256 "$(basename "$asset")" > "$(basename "$asset").sha256" + ) + printf 'dmg=%s\n' "$asset" >> "$GITHUB_OUTPUT" + printf 'checksum=%s.sha256\n' "$asset" >> "$GITHUB_OUTPUT" + + - name: Upload verified assets to draft release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.tag }} + DMG_PATH: ${{ steps.assets.outputs.dmg }} + CHECKSUM_PATH: ${{ steps.assets.outputs.checksum }} + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + notes="$RUNNER_TEMP/desktop-release-notes.md" + cat > "$notes" </dev/null)"; then + test "$(jq -r '.draft' <<< "$release_json")" = "true" + else + gh release create "$RELEASE_TAG" --draft --target "$GITHUB_SHA" --title "CodeGraphy ${version} for macOS" --notes-file "$notes" + fi + gh release upload "$RELEASE_TAG" "$DMG_PATH" "$CHECKSUM_PATH" --clobber + + - name: Remove signing keychain + if: always() && steps.signing.outcome != 'skipped' + shell: bash + run: | + if [[ -n "${KEYCHAIN_PATH:-}" && -f "$KEYCHAIN_PATH" ]]; then + security delete-keychain "$KEYCHAIN_PATH" + fi diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b406d02e0..c59520be6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -10,7 +10,9 @@ "scripts": { "build": "vite build", "build:bundle": "tauri build", + "build:bundle:ad-hoc": "tauri build --target aarch64-apple-darwin --bundles app,dmg --config src-tauri/tauri.ad-hoc.conf.json", "build:sidecar": "node scripts/stage-sidecar.mjs", + "check:bundle": "node scripts/verify-release-bundle.mjs", "dev": "tauri dev --no-watch", "lint": "eslint src scripts vite.config.ts vitest.config.ts", "test": "vitest run --config vitest.config.ts", diff --git a/apps/desktop/scripts/runtime-contract.d.mts b/apps/desktop/scripts/runtime-contract.d.mts new file mode 100644 index 000000000..e4f9e4f3c --- /dev/null +++ b/apps/desktop/scripts/runtime-contract.d.mts @@ -0,0 +1,3 @@ +export const bundledNodeVersion: string; +export const nodeArchiveChecksums: Readonly>; +export const nativeRuntimeModules: readonly string[]; diff --git a/apps/desktop/scripts/runtime-contract.mjs b/apps/desktop/scripts/runtime-contract.mjs new file mode 100644 index 000000000..b4203232e --- /dev/null +++ b/apps/desktop/scripts/runtime-contract.mjs @@ -0,0 +1,29 @@ +export const bundledNodeVersion = '22.23.2'; + +export const nodeArchiveChecksums = { + arm64: '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6', + x64: '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026', +}; + +export const nativeRuntimeModules = [ + '@driftlog/tree-sitter-dart', + '@tree-sitter-grammars/tree-sitter-kotlin', + '@tree-sitter-grammars/tree-sitter-lua/bindings/node/index.js', + 'libsql', + 'tree-sitter', + 'tree-sitter-c', + 'tree-sitter-c-sharp', + 'tree-sitter-cpp', + 'tree-sitter-go', + 'tree-sitter-haskell', + 'tree-sitter-java', + 'tree-sitter-javascript', + 'tree-sitter-objc', + 'tree-sitter-php', + 'tree-sitter-python', + 'tree-sitter-ruby', + 'tree-sitter-rust', + 'tree-sitter-scala', + 'tree-sitter-swift', + 'tree-sitter-typescript', +]; diff --git a/apps/desktop/scripts/sign-native-runtime.d.mts b/apps/desktop/scripts/sign-native-runtime.d.mts new file mode 100644 index 000000000..3c92e0580 --- /dev/null +++ b/apps/desktop/scripts/sign-native-runtime.d.mts @@ -0,0 +1,2 @@ +export function findNativeRuntimeCode(root: string): string[]; +export function signNativeRuntimeCode(root: string, identity: string): string[]; diff --git a/apps/desktop/scripts/sign-native-runtime.mjs b/apps/desktop/scripts/sign-native-runtime.mjs new file mode 100644 index 000000000..d752e73c6 --- /dev/null +++ b/apps/desktop/scripts/sign-native-runtime.mjs @@ -0,0 +1,25 @@ +import { execFileSync } from 'node:child_process'; +import { readdirSync } from 'node:fs'; +import path from 'node:path'; + +export function findNativeRuntimeCode(root) { + const codePaths = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const entryPath = path.join(root, entry.name); + if (entry.isDirectory()) codePaths.push(...findNativeRuntimeCode(entryPath)); + else if (entry.isFile() && (entry.name.endsWith('.node') || entry.name.endsWith('.dylib'))) { + codePaths.push(entryPath); + } + } + return codePaths.sort(); +} + +export function signNativeRuntimeCode(root, identity) { + const codePaths = findNativeRuntimeCode(root); + for (const codePath of codePaths) { + const timestamp = identity === '-' ? '--timestamp=none' : '--timestamp'; + execFileSync('codesign', ['--force', '--sign', identity, timestamp, codePath], { stdio: 'inherit' }); + execFileSync('codesign', ['--verify', '--strict', codePath], { stdio: 'inherit' }); + } + return codePaths; +} diff --git a/apps/desktop/scripts/stage-sidecar.mjs b/apps/desktop/scripts/stage-sidecar.mjs index 625cb85d9..ab2c24bd0 100644 --- a/apps/desktop/scripts/stage-sidecar.mjs +++ b/apps/desktop/scripts/stage-sidecar.mjs @@ -14,12 +14,18 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { pruneDeployedRuntime } from './prune-sidecar-runtime.mjs'; +import { + bundledNodeVersion, + nativeRuntimeModules, + nodeArchiveChecksums, +} from './runtime-contract.mjs'; +import { signNativeRuntimeCode } from './sign-native-runtime.mjs'; const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const repoRoot = path.resolve(appRoot, '../..'); const runtimeRoot = path.join(appRoot, 'src-tauri', 'runtime'); const binaryRoot = path.join(appRoot, 'src-tauri', 'binaries'); -const nodeVersion = '22.23.2'; +const nodeVersion = bundledNodeVersion; const hostTargetByArchitecture = { arm64: 'aarch64-apple-darwin', x64: 'x86_64-apple-darwin', @@ -28,32 +34,6 @@ const nodeDistributionArchitecture = { arm64: 'arm64', x64: 'x64', }; -const nodeArchiveChecksums = { - arm64: '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6', - x64: '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026', -}; -const nativeRuntimeModules = [ - '@driftlog/tree-sitter-dart', - '@tree-sitter-grammars/tree-sitter-kotlin', - '@tree-sitter-grammars/tree-sitter-lua/bindings/node/index.js', - 'libsql', - 'tree-sitter', - 'tree-sitter-c', - 'tree-sitter-c-sharp', - 'tree-sitter-cpp', - 'tree-sitter-go', - 'tree-sitter-haskell', - 'tree-sitter-java', - 'tree-sitter-javascript', - 'tree-sitter-objc', - 'tree-sitter-php', - 'tree-sitter-python', - 'tree-sitter-ruby', - 'tree-sitter-rust', - 'tree-sitter-scala', - 'tree-sitter-swift', - 'tree-sitter-typescript', -]; if (process.platform !== 'darwin') { throw new Error('CodeGraphy desktop sidecars must be staged on macOS.'); @@ -125,12 +105,26 @@ const pruned = pruneDeployedRuntime(path.join(runtimeRoot, 'core'), target); process.stdout.write(`Pruned ${pruned.directories} development directories and ${pruned.files} development files.\n`); copyFileSync(path.join(appRoot, 'scripts', 'core-sidecar.mjs'), path.join(runtimeRoot, 'sidecar.mjs')); +const signingIdentity = process.env.APPLE_SIGNING_IDENTITY ?? '-'; +const signedNativeCode = signNativeRuntimeCode(path.join(runtimeRoot, 'core'), signingIdentity); +process.stdout.write(`Signed ${signedNativeCode.length} native Core modules with the bundle identity.\n`); + mkdirSync(binaryRoot, { recursive: true }); const sidecarPath = path.join(binaryRoot, `codegraphy-core-${target}`); copyFileSync(nodeExecutable, sidecarPath); chmodSync(sidecarPath, 0o755); execFileSync('strip', ['-x', sidecarPath], { stdio: 'inherit' }); -execFileSync('codesign', ['--force', '--sign', '-', '--timestamp=none', sidecarPath], { stdio: 'inherit' }); +const sidecarSigningArguments = signingIdentity === '-' + ? ['--force', '--sign', '-', '--timestamp=none', sidecarPath] + : [ + '--force', + '--sign', signingIdentity, + '--timestamp', + '--options', 'runtime', + '--entitlements', path.join(appRoot, 'src-tauri', 'Entitlements.plist'), + sidecarPath, + ]; +execFileSync('codesign', sidecarSigningArguments, { stdio: 'inherit' }); execFileSync(sidecarPath, ['--version'], { stdio: 'inherit' }); const coreModuleUrl = pathToFileURL(path.join(runtimeRoot, 'core', 'dist', 'index.js')).href; const runtimeProbe = `await Promise.all(${JSON.stringify(nativeRuntimeModules)}.map(module => import(module))); await import(${JSON.stringify(coreModuleUrl)});`; diff --git a/apps/desktop/scripts/verify-release-bundle.d.mts b/apps/desktop/scripts/verify-release-bundle.d.mts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/apps/desktop/scripts/verify-release-bundle.d.mts @@ -0,0 +1 @@ +export {}; diff --git a/apps/desktop/scripts/verify-release-bundle.mjs b/apps/desktop/scripts/verify-release-bundle.mjs new file mode 100644 index 000000000..7c0768c40 --- /dev/null +++ b/apps/desktop/scripts/verify-release-bundle.mjs @@ -0,0 +1,164 @@ +import { execFileSync, spawnSync } from 'node:child_process'; +import { + lstatSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { bundledNodeVersion, nativeRuntimeModules } from './runtime-contract.mjs'; + +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packageManifest = JSON.parse(readFileSync(path.join(appRoot, 'package.json'), 'utf8')); +const target = process.env.CODEGRAPHY_DESKTOP_TARGET ?? 'aarch64-apple-darwin'; +const bundleRoot = process.env.CODEGRAPHY_DESKTOP_BUNDLE_ROOT + ?? path.join(appRoot, 'src-tauri', 'target', target, 'release', 'bundle'); +const appPath = path.join(bundleRoot, 'macos', 'CodeGraphy.app'); +const dmgDirectory = path.join(bundleRoot, 'dmg'); +const signedRelease = process.env.CODEGRAPHY_DESKTOP_REQUIRE_RELEASE_SIGNATURE === '1'; +const maxRuntimeBytes = 220 * 1024 * 1024; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function command(executable, args, options = {}) { + const output = execFileSync(executable, args, { encoding: 'utf8', ...options }); + return typeof output === 'string' ? output.trim() : ''; +} + +function walkFiles(root) { + const files = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const entryPath = path.join(root, entry.name); + if (entry.isSymbolicLink()) { + throw new Error(`Release bundle contains a symbolic link: ${entryPath}`); + } + if (entry.isDirectory()) files.push(...walkFiles(entryPath)); + else if (entry.isFile()) files.push(entryPath); + } + return files; +} + +function directoryBytes(root) { + return walkFiles(root).reduce((total, file) => total + statSync(file).size, 0); +} + +function plistValue(infoPath, key) { + return command('/usr/libexec/PlistBuddy', ['-c', `Print :${key}`, infoPath]); +} + +function signatureDetails(targetPath) { + const result = spawnSync('codesign', ['-dv', '--verbose=4', targetPath], { encoding: 'utf8' }); + assert(result.status === 0, `Unable to inspect signature: ${targetPath}\n${result.stderr}`); + return result.stderr; +} + +function teamIdentifier(targetPath) { + return signatureDetails(targetPath).match(/^TeamIdentifier=(.+)$/mu)?.[1]; +} + +function verifySignature(targetPath, expectedTeam) { + command('codesign', ['--verify', '--strict', '--verbose=2', targetPath], { stdio: 'inherit' }); + if (expectedTeam) { + assert(teamIdentifier(targetPath) === expectedTeam, `Signing team mismatch: ${targetPath}`); + } +} + +function findDmg() { + const images = readdirSync(dmgDirectory) + .filter(name => name.endsWith('.dmg')) + .map(name => path.join(dmgDirectory, name)); + assert(images.length === 1, `Expected one DMG in ${dmgDirectory}; found ${images.length}.`); + return images[0]; +} + +function verifyInstalledImage(dmgPath, expectedTeam) { + const mountPoint = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-dmg-')); + try { + command('diskutil', [ + 'image', + 'attach', + '--mountOptions', 'nobrowse', + '--readOnly', + '--mountPoint', mountPoint, + dmgPath, + ], { stdio: 'inherit' }); + const mountedApp = path.join(mountPoint, 'CodeGraphy.app'); + assert(lstatSync(mountedApp).isDirectory(), 'The DMG does not contain CodeGraphy.app.'); + command('codesign', ['--verify', '--deep', '--strict', '--verbose=2', mountedApp], { stdio: 'inherit' }); + if (expectedTeam) { + assert(teamIdentifier(mountedApp) === expectedTeam, 'The DMG app has the wrong signing team.'); + } + } finally { + try { + command('diskutil', ['eject', mountPoint], { stdio: 'inherit' }); + } finally { + rmSync(mountPoint, { force: true, recursive: true }); + } + } +} + +assert(process.platform === 'darwin', 'Desktop release verification must run on macOS.'); +assert(target === 'aarch64-apple-darwin', `Unsupported release target: ${target}`); +assert(lstatSync(appPath).isDirectory(), `Missing app bundle: ${appPath}`); + +const infoPath = path.join(appPath, 'Contents', 'Info.plist'); +const macOsRoot = path.join(appPath, 'Contents', 'MacOS'); +const resourcesRoot = path.join(appPath, 'Contents', 'Resources', 'runtime'); +const nodePath = path.join(macOsRoot, 'codegraphy-core'); +const appExecutable = path.join(macOsRoot, 'codegraphy-desktop'); +const coreRoot = path.join(resourcesRoot, 'core'); +const coreModule = path.join(coreRoot, 'dist', 'index.js'); +const sidecarScript = path.join(resourcesRoot, 'sidecar.mjs'); + +for (const requiredPath of [infoPath, nodePath, appExecutable, coreModule, sidecarScript]) { + assert(lstatSync(requiredPath).isFile(), `Missing release file: ${requiredPath}`); +} + +assert(plistValue(infoPath, 'CFBundleShortVersionString') === packageManifest.version, 'App version does not match package.json.'); +assert(plistValue(infoPath, 'LSMinimumSystemVersion') === '26.0', 'App minimum system version must be macOS 26.0.'); +assert(command('lipo', ['-archs', nodePath]) === 'arm64', 'Bundled Node must contain only arm64 code.'); +assert(command('lipo', ['-archs', appExecutable]) === 'arm64', 'Desktop executable must contain only arm64 code.'); +assert(command(nodePath, ['--version']) === `v${bundledNodeVersion}`, 'Bundled Node version is incorrect.'); + +const runtimeBytes = directoryBytes(coreRoot) + statSync(nodePath).size; +assert(runtimeBytes <= maxRuntimeBytes, `Bundled Core runtime exceeds 220 MiB: ${runtimeBytes} bytes.`); +const probe = `await Promise.all(${JSON.stringify(nativeRuntimeModules)}.map(module => import(module))); await import(${JSON.stringify(pathToFileURL(coreModule).href)});`; +command(nodePath, ['--input-type=module', '--eval', probe], { cwd: coreRoot, stdio: 'inherit' }); + +command('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], { stdio: 'inherit' }); +const dmgPath = findDmg(); +command('hdiutil', ['verify', dmgPath], { stdio: 'inherit' }); + +let signingTeam; +if (signedRelease) { + signingTeam = teamIdentifier(appPath); + assert(signingTeam && signingTeam !== 'not set', 'Release app does not have a Developer ID signing team.'); + const nestedCode = walkFiles(appPath).filter(file => ( + file === appExecutable + || file === nodePath + || file.endsWith('.dylib') + || file.endsWith('.node') + )); + for (const codePath of nestedCode) verifySignature(codePath, signingTeam); + verifySignature(dmgPath, signingTeam); + command('spctl', ['--assess', '--type', 'execute', '--verbose=4', appPath], { stdio: 'inherit' }); + command('spctl', [ + '--assess', + '--type', 'open', + '--context', 'context:primary-signature', + '--verbose=4', + dmgPath, + ], { stdio: 'inherit' }); + command('xcrun', ['stapler', 'validate', appPath], { stdio: 'inherit' }); + command('xcrun', ['stapler', 'validate', dmgPath], { stdio: 'inherit' }); +} + +verifyInstalledImage(dmgPath, signingTeam); +process.stdout.write(`Verified CodeGraphy ${packageManifest.version} for Apple Silicon (${Math.ceil(runtimeBytes / 1024 / 1024)} MiB Core runtime).\n`); +process.stdout.write(`${dmgPath}\n`); diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index b9f154845..202375f34 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -30,4 +30,5 @@ codegen-units = 1 lto = true opt-level = "s" panic = "abort" -strip = true +# Do not strip the complete Cargo profile. On macOS that also strips host +# proc-macro dylibs, which makes the next release build unable to load them. diff --git a/apps/desktop/src-tauri/Entitlements.ad-hoc.plist b/apps/desktop/src-tauri/Entitlements.ad-hoc.plist new file mode 100644 index 000000000..f2eb2ecb0 --- /dev/null +++ b/apps/desktop/src-tauri/Entitlements.ad-hoc.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/apps/desktop/src-tauri/tauri.ad-hoc.conf.json b/apps/desktop/src-tauri/tauri.ad-hoc.conf.json new file mode 100644 index 000000000..93a2cd497 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.ad-hoc.conf.json @@ -0,0 +1,8 @@ +{ + "bundle": { + "macOS": { + "entitlements": "Entitlements.ad-hoc.plist", + "signingIdentity": "-" + } + } +} diff --git a/apps/desktop/src/sign-native-runtime.test.ts b/apps/desktop/src/sign-native-runtime.test.ts new file mode 100644 index 000000000..ba14f566c --- /dev/null +++ b/apps/desktop/src/sign-native-runtime.test.ts @@ -0,0 +1,34 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { findNativeRuntimeCode } from '../scripts/sign-native-runtime.mjs'; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { force: true, recursive: true }); + } +}); + +function fixture(root: string, relativePath: string): void { + const filePath = path.join(root, relativePath); + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, relativePath); +} + +describe('findNativeRuntimeCode', () => { + it('selects every nested macOS library without selecting JavaScript', () => { + const runtime = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-native-runtime-')); + temporaryDirectories.push(runtime); + fixture(runtime, 'tree-sitter/prebuilds/darwin-arm64/parser.node'); + fixture(runtime, 'libsql/lib/libsql.dylib'); + fixture(runtime, 'core/dist/index.js'); + + expect(findNativeRuntimeCode(runtime)).toEqual([ + path.join(runtime, 'libsql/lib/libsql.dylib'), + path.join(runtime, 'tree-sitter/prebuilds/darwin-arm64/parser.node'), + ]); + }); +}); diff --git a/tests/release/desktopPackage.test.mjs b/tests/release/desktopPackage.test.mjs new file mode 100644 index 000000000..2b6af5c07 --- /dev/null +++ b/tests/release/desktopPackage.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const desktopManifest = JSON.parse(readFileSync('apps/desktop/package.json', 'utf8')); +const tauriConfiguration = JSON.parse(readFileSync('apps/desktop/src-tauri/tauri.conf.json', 'utf8')); +const cargoManifest = readFileSync('apps/desktop/src-tauri/Cargo.toml', 'utf8'); +const releaseWorkflow = readFileSync('.github/workflows/desktop-release.yml', 'utf8'); +const releaseEntitlements = readFileSync('apps/desktop/src-tauri/Entitlements.plist', 'utf8'); +const adHocConfiguration = JSON.parse(readFileSync('apps/desktop/src-tauri/tauri.ad-hoc.conf.json', 'utf8')); +const adHocEntitlements = readFileSync('apps/desktop/src-tauri/Entitlements.ad-hoc.plist', 'utf8'); + +function cargoPackageVersion() { + const packageSection = cargoManifest.match(/^\[package\]\n([\s\S]*?)(?=^\[)/mu)?.[1]; + return packageSection?.match(/^version = "([^"]+)"$/mu)?.[1]; +} + +test('desktop package versions have one release identity', () => { + assert.equal(cargoPackageVersion(), desktopManifest.version); + assert.equal(tauriConfiguration.version, desktopManifest.version); +}); + +test('desktop bundle contract is macOS 26 with app and DMG assets', () => { + assert.equal(tauriConfiguration.bundle.macOS.minimumSystemVersion, '26.0'); + assert.deepEqual(tauriConfiguration.bundle.targets, ['app', 'dmg']); + assert.equal(tauriConfiguration.bundle.macOS.hardenedRuntime, true); + assert.equal(tauriConfiguration.bundle.macOS.signingIdentity, undefined); + assert.doesNotMatch(cargoManifest, /^strip = true$/mu); + assert.doesNotMatch(releaseEntitlements, /disable-library-validation/u); + assert.equal(adHocConfiguration.bundle.macOS.signingIdentity, '-'); + assert.match(adHocEntitlements, /disable-library-validation/u); +}); + +test('desktop release stays gated until signed artifacts pass verification', () => { + assert.match(releaseWorkflow, /workflow_dispatch:/u); + assert.match(releaseWorkflow, /runs-on: macos-26/u); + assert.match(releaseWorkflow, /environment: desktop-release/u); + assert.match(releaseWorkflow, /CODEGRAPHY_DESKTOP_REQUIRE_RELEASE_SIGNATURE: '1'/u); + assert.match(releaseWorkflow, /test "\$GITHUB_REF" = "refs\/heads\/main"/u); + assert.match(releaseWorkflow, /xcodebuild -version \| grep -q '\^Xcode 26\\\.'/u); + assert.match(releaseWorkflow, /Verify release bundle/u); + assert.match(releaseWorkflow, /notarytool submit "\$dmg"/u); + assert.match(releaseWorkflow, /stapler staple "\$dmg"/u); + assert.match(releaseWorkflow, /gh release create "\$RELEASE_TAG" --draft/u); + assert.doesNotMatch(releaseWorkflow, /gh release edit .*--draft=false/u); +}); From 921067ef55ba5e184139a068c6de58a60f2f24d0 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 13:55:16 -0700 Subject: [PATCH 04/25] docs(desktop): document release gate and website status --- .changeset/quiet-desktop-graph.md | 5 ++ CONTEXT.md | 2 + README.md | 12 +++- apps/desktop/README.md | 71 +++++++++++++++++++++++ apps/web/app/_components/get-started.tsx | 26 +++++++-- apps/web/app/_components/header.tsx | 5 +- apps/web/components/footer.tsx | 2 + apps/web/components/link.tsx | 4 +- apps/web/content/docs.ts | 12 ++++ apps/web/content/links.ts | 7 +++ apps/web/content/llms.ts | 5 +- apps/web/content/site.ts | 2 +- apps/web/tsconfig.json | 12 ++++ docs/README.md | 2 + docs/RELEASING.md | 72 +++++++++++++++++++++++- tests/release/desktopPackage.test.mjs | 10 ++++ 16 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 .changeset/quiet-desktop-graph.md create mode 100644 apps/desktop/README.md diff --git a/.changeset/quiet-desktop-graph.md b/.changeset/quiet-desktop-graph.md new file mode 100644 index 000000000..a90e96586 --- /dev/null +++ b/.changeset/quiet-desktop-graph.md @@ -0,0 +1,5 @@ +--- +"@codegraphy-dev/core": patch +--- + +Expose a cached CodeGraphy Workspace Relationship Graph request for local interfaces. diff --git a/CONTEXT.md b/CONTEXT.md index fefdb4dbf..f6ce3edd9 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -127,6 +127,7 @@ Full and incremental updates acquire writer ownership before their final discove | Surface | Ownership | |---|---| | **Core Package** | `@codegraphy-dev/core` owns headless Indexing, Graph Cache storage, plugin processing, Graph Query, and the `codegraphy` CLI. | +| **macOS Desktop App** | `@codegraphy-dev/desktop` owns the Tauri window, File and Folder hierarchy, lightweight editor, safe File access, Core process lifecycle, and adapters over Core and the graph renderer. | | **VS Code Extension** | Owns VS Code lifecycle, the Graph View, editor actions, workspace settings UI, and adapters over Core and the renderer. | | **tldraw Interface** | `@codegraphy-dev/tldraw` owns its launcher, tldraw document lifecycle, native shapes, controls, and adapters over Core and renderer physics. | | **Graph Renderer** | `@codegraphy-dev/graph-renderer` owns WebGPU drawing and deterministic WebAssembly physics. It does not own product settings, persistence, or plugins. | @@ -185,6 +186,7 @@ Extension chrome inherits the active VS Code theme. Graph Data Color may encode ## Package Boundaries +- `apps/desktop` owns the macOS product surface and its local Core process boundary. - `packages/core` owns shared engine behavior and the CLI. - `packages/extension` owns the VS Code product surface. - `packages/tldraw` owns the tldraw offline product surface and launcher. diff --git a/README.md b/README.md index 5f0c6d02d..7494e5adf 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ CodeGraphy indexes a folder and projects its files and declarations into Nodes. It renders imports, calls, references, inheritance, containment, tests, and plugin-defined Relationships. -Explore the graph inside VS Code or as native shapes in tldraw offline. Search, Graph Scope, and persistent filters narrow the VS Code view. The Core engine also supports the terminal CLI and agent queries. +Explore the graph in the macOS desktop app, inside VS Code, or as native shapes in tldraw offline. Search, Graph Scope, and persistent filters narrow the VS Code view. The Core engine also supports the terminal CLI and agent queries. ![CodeGraphy Relationship Graph interaction demo](./docs/media/readme/relationship-graph-demo.gif) @@ -48,6 +48,7 @@ Join the [CodeGraphy Discord](https://discord.gg/Z75vbkt4Ry) for installation he | Capability | What it provides | |---|---| | Relationship Graph | File, folder, package, Symbol, and plugin-defined Nodes connected by typed Edges. | +| macOS desktop app | A three-pane File hierarchy, CodeMirror editor, and WebGPU Relationship Graph backed by local Core. | | Search and filters | Temporary search plus workspace-local include and exclude rules. | | Graph Scope | One panel for Node Type and Edge Type visibility. | | Symbol Nodes | Functions, classes, interfaces, types, variables, constants, and language-specific declarations. | @@ -72,6 +73,12 @@ Join the [CodeGraphy Discord](https://discord.gg/Z75vbkt4Ry) for installation he ## Install +### macOS desktop app + +The Apple Silicon desktop app requires macOS 26 or later. The source, app bundle, DMG path, and release checks are in this repository, but there is no public download yet. Developer ID signing, notarization, and the installed-app acceptance check still gate the first release. + +Do not treat an ad-hoc development DMG as a production artifact. See the [desktop app guide](./apps/desktop/README.md) for current behavior and the exact release gate. + ### VS Code Extension 1. Install [CodeGraphy from the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=codegraphy.codegraphy). @@ -199,12 +206,13 @@ A public `codegraphy/skills` repository will host the skill once published. `@codegraphy-dev/core` owns File Discovery, built-in analysis, Graph Cache watching, plugin activation, SQLite storage, Graph Query, and the CLI. It does not own rendering. -The VS Code extension connects Core to the editor lifecycle and React Graph View. It changes cached source facts only after an explicit Index or Re-index Workspace action. The tldraw interface connects Core data and shared physics to native tldraw shapes. +The macOS desktop app runs Core in a bundled local process and keeps File access and child-process control in Rust. Its webview combines a File hierarchy, CodeMirror, and the existing renderer. The VS Code extension connects Core to the editor lifecycle and React Graph View. The tldraw interface connects Core data and shared physics to native tldraw shapes. `@codegraphy-dev/graph-renderer` owns WebGPU drawing and WebAssembly physics. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. | Package | Role | |---|---| +| [`@codegraphy-dev/desktop`](./apps/desktop/README.md) | Tauri macOS app, local Core process boundary, File hierarchy, and editor. | | [`@codegraphy-dev/core`](./packages/core/README.md) | Shared indexing, cache, plugin, query, and CLI engine. | | [`@codegraphy-dev/extension`](./packages/extension/docs/README.md) | VS Code host and Graph View product integration. | | [`@codegraphy-dev/tldraw`](./packages/tldraw/README.md) | macOS launcher and native tldraw offline canvas integration. | diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 000000000..4a517c21f --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,71 @@ +# CodeGraphy for macOS + +CodeGraphy for macOS is a focused local code navigator. It puts the File and Folder hierarchy, a CodeMirror editor, and the Core-owned Relationship Graph in one window. + +There is no public download yet. The Apple Silicon app and DMG build locally, but the first public artifact still needs Developer ID signing, Apple notarization, and an installed-app acceptance check. Do not distribute an ad-hoc build as a release. + +## Requirements + +- Apple Silicon Mac +- macOS 26 or later +- WebGPU support in the system WKWebView + +Intel is not a supported release target yet. The graph renderer has no non-WebGPU implementation, so macOS 25 and earlier are not supported. + +## Current behavior + +- Open any local folder as a CodeGraphy Workspace. +- Browse a thin File and Folder hierarchy. +- Open UTF-8 text Files up to 5 MiB in CodeMirror. +- Save through an atomic replacement that preserves permissions and rejects an external edit conflict. +- Read or rebuild the workspace-owned `.codegraphy/graph.sqlite` Graph Cache through Core. +- Apply one-File incremental Indexing after a save. +- Show File Nodes, optional Symbol Nodes, and Relationships with the existing WebGPU and WebAssembly graph renderer. + +Source Files and the Graph Cache stay in the workspace. The app does not upload them. + +## Architecture + +The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. + +Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. The right pane uses `@codegraphy-dev/graph-renderer` without adding desktop behavior to the renderer package. + +See the [stack decision](../../docs/research/desktop-app-stack.md) for the comparison and release constraints. + +## Development + +Run setup from the repository root: + +```bash +pnpm install --frozen-lockfile +``` + +Start the app with the desktop package as the working directory: + +```bash +pnpm --filter @codegraphy-dev/desktop dev +``` + +`build:sidecar` downloads the pinned Node archive, checks its SHA-256 digest, deploys the built Core runtime, removes development-only files, signs the staged native code, and imports Core plus every native parser before Tauri starts. + +Use the focused checks while changing the app: + +```bash +pnpm --filter @codegraphy-dev/desktop test +pnpm --filter @codegraphy-dev/desktop typecheck +cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib +``` + +## Local package check + +Use Xcode 26. Xcode 27 beta currently produces malformed release proc-macro binaries with Rust 1.96 on this project. + +```bash +DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \ + pnpm --filter @codegraphy-dev/desktop build:bundle:ad-hoc +pnpm --filter @codegraphy-dev/desktop check:bundle +``` + +The ad-hoc configuration disables library validation because ad-hoc signatures have no shared Apple team. The production configuration does not have that entitlement. The verifier checks the app, the mounted DMG, the bundled Node and Core versions, all native modules, architecture, minimum macOS version, signatures, and a 220 MiB uncompressed runtime budget. + +Follow the [desktop release procedure](../../docs/RELEASING.md#macos-desktop-release) for production signing, notarization, draft assets, and the installed-app gate. diff --git a/apps/web/app/_components/get-started.tsx b/apps/web/app/_components/get-started.tsx index 6880d50c7..4ba583130 100644 --- a/apps/web/app/_components/get-started.tsx +++ b/apps/web/app/_components/get-started.tsx @@ -3,12 +3,21 @@ import { CopyButton } from '@/components/copy-button'; import { Link } from '@/components/link'; import { SectionHeader } from '@/components/section-header'; import { buttonVariants } from '@/components/ui/button'; -import { githubBlobHref, marketplaceHref, pluginsHref, vscodeExtensionHref } from '@/content/links'; +import { desktopRelease, githubBlobHref, marketplaceHref, pluginsHref, vscodeExtensionHref } from '@/content/links'; import { cn } from '@/lib/utils'; const paths = [ { number: '01', + title: 'Navigate on macOS', + summary: 'Open a local workspace in the three-pane desktop app. Browse Files, make a lightweight edit, and inspect the Core-owned Relationship Graph.', + href: desktopRelease.sourceHref, + label: 'Read the release status', + status: desktopRelease.label, + icon: 'github' as const, + }, + { + number: '02', title: 'See it in VS Code', summary: 'Install the extension, open a CodeGraphy Workspace, run Indexing, and explore the Relationship Graph beside your code.', href: vscodeExtensionHref, @@ -16,7 +25,7 @@ const paths = [ icon: 'vscode' as const, }, { - number: '02', + number: '03', title: 'Query it from the terminal', summary: 'Install @codegraphy-dev/core when you want headless Indexing, diagnostics, Graph Query, Graph Scope, and Filter commands.', href: `${githubBlobHref}/packages/core/README.md`, @@ -24,7 +33,7 @@ const paths = [ icon: 'github' as const, }, { - number: '03', + number: '04', title: 'Teach it more', summary: 'Enable official Plugins or build a typed Plugin Package to add framework, engine, document, or language-specific graph meaning.', href: pluginsHref, @@ -40,8 +49,8 @@ export function GetStarted(): React.ReactElement {
@@ -50,12 +59,17 @@ export function GetStarted(): React.ReactElement {
-
    +
      {paths.map((path) => (
    1. {path.number}

      {path.title}

      {path.summary}

      + {'status' in path ? ( +

      + {path.status} +

      + ) : null} {path.label} diff --git a/apps/web/app/_components/header.tsx b/apps/web/app/_components/header.tsx index ddae0b315..3f57ea6f0 100644 --- a/apps/web/app/_components/header.tsx +++ b/apps/web/app/_components/header.tsx @@ -5,7 +5,7 @@ import { Liquid } from '@/components/canvasui/Liquid'; import { Link } from '@/components/link'; import { ThemeHeroImage } from '@/components/theme-hero-image'; import { buttonVariants } from '@/components/ui/button'; -import { examplesHref, githubHref, vscodeExtensionHref } from '@/content/links'; +import { desktopRelease, examplesHref, githubHref, vscodeExtensionHref } from '@/content/links'; import { cn } from '@/lib/utils'; import { useRef } from 'react'; import { HeroGraph } from './hero-graph'; @@ -76,6 +76,9 @@ export function Header(): React.ReactElement { Explore the source
+

+ {desktopRelease.minimumSystemVersion} · {desktopRelease.label} +

$ npm i -g @codegraphy-dev/core diff --git a/apps/web/components/footer.tsx b/apps/web/components/footer.tsx index f514ccc90..695c1900b 100644 --- a/apps/web/components/footer.tsx +++ b/apps/web/components/footer.tsx @@ -2,6 +2,7 @@ import Image from 'next/image'; import { Link, type LinkIcon } from '@/components/link'; import { docsHref, + desktopRelease, discordHref, examplesHref, githubHref, @@ -91,6 +92,7 @@ export function Footer(): React.ReactElement {

© {year} Joe Soboleski

+

{desktopRelease.label}

MIT licensed

diff --git a/apps/web/components/link.tsx b/apps/web/components/link.tsx index fc5e02b73..65a5b841f 100644 --- a/apps/web/components/link.tsx +++ b/apps/web/components/link.tsx @@ -1,5 +1,5 @@ import NextLink from 'next/link'; -import type { AnchorHTMLAttributes, ReactNode } from 'react'; +import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react'; import { Icon } from '@/components/icon'; import { cn } from '@/lib/utils'; @@ -27,7 +27,7 @@ export function Link({ target, variant, ...anchorProps -}: LinkProps): React.ReactElement { +}: LinkProps): ReactElement { const external = /^[a-z][a-z0-9+.-]*:/i.test(href); const opensInNewTab = href.startsWith('http://') || href.startsWith('https://'); const classNames = cn( diff --git a/apps/web/content/docs.ts b/apps/web/content/docs.ts index 6e36a45db..ceb017273 100644 --- a/apps/web/content/docs.ts +++ b/apps/web/content/docs.ts @@ -22,6 +22,12 @@ export const docsGroups: readonly DocsGroup[] = [ href: `${githubBlobHref}/docs/README.md`, summary: 'Start here for the project docs index and the main reference map.', }, + { + id: 'desktop', + title: 'macOS desktop app', + href: `${githubBlobHref}/apps/desktop/README.md`, + summary: 'Current three-pane behavior, macOS requirements, local packaging, and the signed-download gate.', + }, { id: 'commands', title: 'Commands', @@ -106,6 +112,12 @@ export const docsGroups: readonly DocsGroup[] = [ title: 'Packages', summary: 'READMEs for the packages that make up the monorepo.', links: [ + { + id: 'desktop-package', + title: 'Desktop app', + href: `${githubBlobHref}/apps/desktop/README.md`, + summary: 'Tauri shell, local Core process, File editor, Relationship Graph, and macOS package checks.', + }, { id: 'core-package', title: 'Core package', diff --git a/apps/web/content/links.ts b/apps/web/content/links.ts index 70130d508..8ac033bee 100644 --- a/apps/web/content/links.ts +++ b/apps/web/content/links.ts @@ -8,6 +8,13 @@ export const discordHref = 'https://discord.gg/Z75vbkt4Ry'; export const githubBlobHref = `${githubHref}/blob/main`; export const githubTreeHref = `${githubHref}/tree/main`; +export const desktopRelease = { + available: false, + label: 'Signed macOS download pending', + minimumSystemVersion: 'macOS 26', + sourceHref: `${githubTreeHref}/apps/desktop`, +} as const; + export const marketplaceHref = 'https://marketplace.visualstudio.com/items?itemName=codegraphy.codegraphy'; export const vscodeExtensionHref = 'vscode:extension/codegraphy.codegraphy'; diff --git a/apps/web/content/llms.ts b/apps/web/content/llms.ts index 660247b71..2eec790b2 100644 --- a/apps/web/content/llms.ts +++ b/apps/web/content/llms.ts @@ -5,7 +5,7 @@ export function renderLlmsTxt(siteUrl: URL): string { > CodeGraphy builds a local Relationship Graph of the files, symbols, packages, and connections in a CodeGraphy Workspace. -CodeGraphy provides one graph for the VS Code extension, Core CLI, coding agents, and Plugins. Indexing writes a SQLite Graph Cache on disk. Source code stays in the workspace. +CodeGraphy provides one graph for the macOS desktop app, VS Code extension, Core CLI, coding agents, and Plugins. Indexing writes a SQLite Graph Cache on disk. Source code stays in the workspace. The desktop source and package checks are available, but the signed macOS download is pending release verification. ## Start here @@ -27,7 +27,7 @@ The CodeGraphy Agent Skill teaches shell-capable agents to use the narrowest use ## Website -- [Home](${siteOrigin}/): Product overview and VS Code installation. +- [Home](${siteOrigin}/): Product overview, VS Code installation, and macOS release status. - [Docs](${siteOrigin}/docs): Product, CLI, settings, and Plugin documentation. - [Plugins](${siteOrigin}/plugins): Official Plugin packages and installation steps. - [Examples](${siteOrigin}/examples): Runnable example workspaces and their graphs. @@ -35,6 +35,7 @@ The CodeGraphy Agent Skill teaches shell-capable agents to use the narrowest use ## Primary documentation - [Project docs](https://github.com/joesobo/CodeGraphyV4/blob/main/docs/README.md) +- [macOS desktop app](https://github.com/joesobo/CodeGraphyV4/blob/main/apps/desktop/README.md) - [Core CLI](https://github.com/joesobo/CodeGraphyV4/blob/main/packages/core/README.md) - [Commands](https://github.com/joesobo/CodeGraphyV4/blob/main/docs/COMMANDS.md) - [Settings](https://github.com/joesobo/CodeGraphyV4/blob/main/docs/SETTINGS.md) diff --git a/apps/web/content/site.ts b/apps/web/content/site.ts index c8c142db8..fb4bdba8d 100644 --- a/apps/web/content/site.ts +++ b/apps/web/content/site.ts @@ -4,7 +4,7 @@ const defaultSiteOrigin = 'https://codegraphy.dev'; export const siteOrigin = resolvePublicOrigin(process.env.NEXT_PUBLIC_SITE_URL); export const siteUrl = new URL(siteOrigin); export const siteDescription = - 'A local Relationship Graph for exploring files, symbols, packages, and their connections in VS Code or from the command line.'; + 'A local Relationship Graph for exploring files, symbols, packages, and their connections. Use VS Code or the Core CLI today. The macOS desktop release is in signing verification.'; function resolvePublicOrigin(configuredOrigin: string | undefined): string { const candidate = configuredOrigin ?? defaultSiteOrigin; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 9cfbbc7c1..d5e75fff2 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -19,6 +19,18 @@ "paths": { "@/*": [ "./*" + ], + "react": [ + "./node_modules/@types/react" + ], + "react/*": [ + "./node_modules/@types/react/*" + ], + "react-dom": [ + "./node_modules/@types/react-dom" + ], + "react-dom/*": [ + "./node_modules/@types/react-dom/*" ] }, "plugins": [ diff --git a/docs/README.md b/docs/README.md index b81467954..738f959d3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ Start with the root [README](../README.md) for installation, the product overvie | Document | Covers | |---|---| +| [macOS desktop app](../apps/desktop/README.md) | Three-pane app behavior, Core process boundary, development, packaging, and release gate. | | [Settings](./SETTINGS.md) | `.codegraphy/settings.json`, Graph Scope, Display, Forces, Performance, CSS Snippets, and workspace state. | | [Interactions](./INTERACTIONS.md) | Node, Graph Stage, minimap, context menu, tool rail, panel, and export behavior. | | [Commands](./COMMANDS.md) | VS Code Command Palette commands and IDs. | @@ -21,6 +22,7 @@ The root [domain glossary](../CONTEXT.md) defines shared CodeGraphy language and | Area | Where to start | |---|---| +| macOS desktop app | [`apps/desktop/README.md`](../apps/desktop/README.md) | | Core engine and CLI | [`packages/core/README.md`](../packages/core/README.md) | | VS Code extension | [`packages/extension/docs/README.md`](../packages/extension/docs/README.md) | | Graph renderer | [`packages/graph-renderer/README.md`](../packages/graph-renderer/README.md) | diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 32b409cf9..200787d74 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,11 +1,12 @@ # Releasing -CodeGraphy publishes one VS Code extension plus Core and plugin npm packages. The Agent Skill currently ships as source in this repository and is prepared for a future dedicated repository. +CodeGraphy publishes a macOS desktop app, one VS Code extension, Core, and plugin npm packages. The Agent Skill currently ships as source in this repository and is prepared for a future dedicated repository. ## Release Surfaces | Surface | Source of version and metadata | |---|---| +| macOS desktop app | `apps/desktop/package.json`, `apps/desktop/src-tauri/Cargo.toml`, and `apps/desktop/src-tauri/tauri.conf.json` must have the same version. | | VS Code extension | Root `package.json` supplies Marketplace metadata; `packages/extension/package.json` supplies the release version. | | Core CLI | `packages/core/package.json` | | Graph renderer | `packages/graph-renderer/package.json` | @@ -15,6 +16,75 @@ CodeGraphy publishes one VS Code extension plus Core and plugin npm packages. Th The root package version stays pinned as monorepo workspace metadata. `scripts/release-core.mjs` builds the Marketplace manifest with the extension package version. +## macOS desktop release + +The desktop workflow creates an Apple Silicon DMG for macOS 26 or later. It does not publish the GitHub Release. A maintainer must finish the installed-app check and publish the draft. + +### Required release state + +- Run from `main` with no uncommitted changes. +- Keep the desktop package, Cargo, and Tauri versions identical. +- Use the `desktop-v` tag, for example `desktop-v0.1.0`. +- Use the protected `desktop-release` GitHub environment. +- Keep the website download control in its pending state until the draft passes the installed-app check and becomes public. + +The workflow needs these GitHub environment secrets: + +- `APPLE_CERTIFICATE`: base64-encoded Developer ID Application `.p12` file +- `APPLE_CERTIFICATE_PASSWORD`: password for that `.p12` file +- `APPLE_API_ISSUER`: App Store Connect API issuer ID +- `APPLE_API_KEY`: App Store Connect API key ID +- `APPLE_API_KEY_P8`: complete private API key file contents + +Only the Apple Developer Account Holder can create the Developer ID Application certificate. Keep the certificate and API private key out of the repository. + +### Local package check + +The local command uses an ad-hoc identity. It proves the package structure and runtime, but it does not create a distributable artifact. + +```bash +DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \ + pnpm --filter @codegraphy-dev/desktop build:bundle:ad-hoc +pnpm --filter @codegraphy-dev/desktop check:bundle +``` + +Use Xcode 26. The workflow runs on GitHub's Apple Silicon `macos-26` image and rejects a different Xcode major version. The verifier requires arm64-only executables, macOS 26.0, matching versions, a valid app and DMG, a mounted-image app, a complete native Core import, and no symbolic links in the bundle. + +Never upload the ad-hoc DMG to a release or link it from the website. + +### Build the draft + +Run the `Desktop release` workflow from `main` and enter the exact desktop tag. The workflow: + +1. Confirms the tag, branch, Apple Silicon host, and Xcode 26. +2. Runs desktop unit tests, TypeScript typecheck, and Rust tests. +3. Imports the Developer ID certificate into a temporary keychain. +4. Builds Core and the React app, stages pinned Node 22.23.2, prunes the Core runtime, and signs all native modules with the bundle identity. +5. Builds and signs the app with Hardened Runtime, then notarizes and staples it through Tauri. +6. Signs the final DMG, submits it to `notarytool`, and staples the accepted ticket. +7. Checks nested signatures and team IDs, Gatekeeper, stapling, DMG integrity, mounted-image integrity, architecture, versions, and native module loading. +8. Creates or updates a draft GitHub Release and uploads the verified DMG plus its SHA-256 file. + +The workflow refuses an existing non-draft release. It removes the temporary signing keychain even when a previous step fails. + +### Installed-app acceptance check + +Run this check on the draft asset before you publish it: + +1. Download the DMG and `.sha256` file from the draft GitHub Release on another Apple Silicon Mac that runs macOS 26 or later. +2. Verify the SHA-256 file, mount the DMG, and copy `CodeGraphy.app` to `/Applications`. +3. Run `codesign --verify --deep --strict --verbose=2 /Applications/CodeGraphy.app`. +4. Run `spctl --assess --type execute --verbose=4 /Applications/CodeGraphy.app`. +5. Run `xcrun stapler validate /Applications/CodeGraphy.app` and validate the downloaded DMG too. +6. Launch the installed app through Finder. Confirm macOS shows no unsigned or damaged-app warning. +7. Open a real CodeGraphy Workspace. Confirm the File and Folder hierarchy appears. +8. Open a source File, edit it, save it, and confirm Core reports one-File incremental Indexing. +9. Turn on Symbol Nodes and confirm the right pane draws the Core-owned Relationship Graph with WebGPU. +10. Change the open File outside CodeGraphy, then confirm the app rejects an overwrite until the File is reopened. +11. Close the app and confirm its `codegraphy-core` child process exits. + +Keep the release as a draft if any step fails. After all steps pass, publish the GitHub Release, update the website to link the exact public DMG, and verify that link from a clean browser session. The website must not point at a draft, a workflow artifact, or an ad-hoc build. + ## Prepare a Release Use a clean checkout of `main` with an active Node.js LTS release and the repository-pinned pnpm version. diff --git a/tests/release/desktopPackage.test.mjs b/tests/release/desktopPackage.test.mjs index 2b6af5c07..493088080 100644 --- a/tests/release/desktopPackage.test.mjs +++ b/tests/release/desktopPackage.test.mjs @@ -9,6 +9,8 @@ const releaseWorkflow = readFileSync('.github/workflows/desktop-release.yml', 'u const releaseEntitlements = readFileSync('apps/desktop/src-tauri/Entitlements.plist', 'utf8'); const adHocConfiguration = JSON.parse(readFileSync('apps/desktop/src-tauri/tauri.ad-hoc.conf.json', 'utf8')); const adHocEntitlements = readFileSync('apps/desktop/src-tauri/Entitlements.ad-hoc.plist', 'utf8'); +const websiteLinks = readFileSync('apps/web/content/links.ts', 'utf8'); +const websiteDesktopCallToAction = readFileSync('apps/web/app/_components/get-started.tsx', 'utf8'); function cargoPackageVersion() { const packageSection = cargoManifest.match(/^\[package\]\n([\s\S]*?)(?=^\[)/mu)?.[1]; @@ -44,3 +46,11 @@ test('desktop release stays gated until signed artifacts pass verification', () assert.match(releaseWorkflow, /gh release create "\$RELEASE_TAG" --draft/u); assert.doesNotMatch(releaseWorkflow, /gh release edit .*--draft=false/u); }); + +test('website does not advertise an unavailable desktop artifact', () => { + assert.match(websiteLinks, /available: false/u); + assert.match(websiteLinks, /Signed macOS download pending/u); + assert.doesNotMatch(websiteLinks, /releases\/download/u); + assert.match(websiteDesktopCallToAction, /Read the release status/u); + assert.doesNotMatch(websiteDesktopCallToAction, /Download for macOS/u); +}); From febf6d048a99446c162e3b64054a5dadabf1c4d1 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 14:04:17 -0700 Subject: [PATCH 05/25] fix(desktop): harden React lifecycle boundaries --- apps/desktop/src/components/CodeEditor.tsx | 118 ++++++------------ apps/desktop/src/components/GraphPanel.tsx | 2 +- .../src/components/createCodeEditor.ts | 93 ++++++++++++++ apps/web/components/link.tsx | 4 +- apps/web/tsconfig.json | 13 +- 5 files changed, 132 insertions(+), 98 deletions(-) create mode 100644 apps/desktop/src/components/createCodeEditor.ts diff --git a/apps/desktop/src/components/CodeEditor.tsx b/apps/desktop/src/components/CodeEditor.tsx index 650cd15d9..26c4f274b 100644 --- a/apps/desktop/src/components/CodeEditor.tsx +++ b/apps/desktop/src/components/CodeEditor.tsx @@ -1,35 +1,6 @@ -import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; -import { javascript } from '@codemirror/lang-javascript'; -import { - bracketMatching, - defaultHighlightStyle, - indentOnInput, - syntaxHighlighting, -} from '@codemirror/language'; -import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; -import { EditorState } from '@codemirror/state'; -import { - drawSelection, - dropCursor, - EditorView, - highlightActiveLine, - highlightActiveLineGutter, - keymap, - lineNumbers, -} from '@codemirror/view'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { FileDocument } from '../bridge'; -function languageForPath(path: string) { - if (/\.[cm]?[jt]sx?$/iu.test(path)) { - return javascript({ - jsx: /x$/iu.test(path), - typescript: /\.[cm]?tsx?$/iu.test(path), - }); - } - return []; -} - export function CodeEditor({ document, onChange, @@ -42,62 +13,43 @@ export function CodeEditor({ const hostRef = useRef(null); const onChangeRef = useRef(onChange); const onSaveRef = useRef(onSave); - onChangeRef.current = onChange; - onSaveRef.current = onSave; + const [loadError, setLoadError] = useState(); + + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + useEffect(() => { + onSaveRef.current = onSave; + }, [onSave]); useEffect(() => { const host = hostRef.current; if (!host) return; - const view = new EditorView({ - parent: host, - state: EditorState.create({ - doc: document.content, - extensions: [ - lineNumbers(), - highlightActiveLineGutter(), - history(), - drawSelection(), - dropCursor(), - EditorState.allowMultipleSelections.of(true), - indentOnInput(), - bracketMatching(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - highlightSelectionMatches(), - highlightActiveLine(), - languageForPath(document.path), - keymap.of([ - { key: 'Mod-s', run: () => { onSaveRef.current(); return true; } }, - indentWithTab, - ...defaultKeymap, - ...historyKeymap, - ...searchKeymap, - ]), - EditorView.lineWrapping, - EditorView.updateListener.of((update) => { - if (update.docChanged) onChangeRef.current(update.state.doc.toString()); - }), - EditorView.theme({ - '&': { height: '100%', backgroundColor: '#0c161d', color: '#dbe8ee' }, - '.cm-content': { caretColor: '#73e2c4', padding: '18px 0 40px' }, - '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#73e2c4' }, - '.cm-gutters': { - backgroundColor: '#0c161d', - borderRight: '1px solid rgba(149, 180, 190, .1)', - color: '#49616d', - }, - '.cm-activeLine': { backgroundColor: 'rgba(110, 221, 193, .045)' }, - '.cm-activeLineGutter': { backgroundColor: 'rgba(110, 221, 193, .07)', color: '#88a5af' }, - '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { - backgroundColor: 'rgba(69, 141, 168, .35)', - }, - '.cm-scroller': { fontFamily: 'var(--font-code)', fontSize: '12.5px', lineHeight: '1.68' }, - }), - ], - }), - }); - view.focus(); - return () => view.destroy(); - }, [document.path, document.revision]); + let active = true; + let destroy: (() => void) | undefined; + setLoadError(undefined); + void import('./createCodeEditor') + .then(({ createCodeEditor }) => { + if (!active) return; + destroy = createCodeEditor({ + content: document.content, + onChange: content => onChangeRef.current(content), + onSave: () => onSaveRef.current(), + parent: host, + path: document.path, + }); + }) + .catch((error: unknown) => { + if (active) setLoadError(error instanceof Error ? error.message : String(error)); + }); + return () => { + active = false; + destroy?.(); + }; + }, [document.content, document.path, document.revision]); - return
; + return loadError + ?
Editor unavailable: {loadError}
+ :
; } diff --git a/apps/desktop/src/components/GraphPanel.tsx b/apps/desktop/src/components/GraphPanel.tsx index ab68979fb..12acec561 100644 --- a/apps/desktop/src/components/GraphPanel.tsx +++ b/apps/desktop/src/components/GraphPanel.tsx @@ -63,13 +63,13 @@ export function GraphPanel({ const redrawRef = useRef<(() => void) | undefined>(undefined); const [physicsReady, setPhysicsReady] = useState(false); const [rendererError, setRendererError] = useState(); - selectedIdRef.current = selectedId; const relationships = useMemo(() => graph.edges.filter( edge => edge.from === selectedId || edge.to === selectedId, ).slice(0, 8), [graph.edges, selectedId]); useEffect(() => { + selectedIdRef.current = selectedId; redrawRef.current?.(); }, [selectedId]); diff --git a/apps/desktop/src/components/createCodeEditor.ts b/apps/desktop/src/components/createCodeEditor.ts new file mode 100644 index 000000000..35514df94 --- /dev/null +++ b/apps/desktop/src/components/createCodeEditor.ts @@ -0,0 +1,93 @@ +import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; +import { javascript } from '@codemirror/lang-javascript'; +import { + bracketMatching, + defaultHighlightStyle, + indentOnInput, + syntaxHighlighting, +} from '@codemirror/language'; +import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; +import { EditorState } from '@codemirror/state'; +import { + drawSelection, + dropCursor, + EditorView, + highlightActiveLine, + highlightActiveLineGutter, + keymap, + lineNumbers, +} from '@codemirror/view'; + +function languageForPath(path: string) { + if (/\.[cm]?[jt]sx?$/iu.test(path)) { + return javascript({ + jsx: /x$/iu.test(path), + typescript: /\.[cm]?tsx?$/iu.test(path), + }); + } + return []; +} + +export function createCodeEditor({ + content, + onChange, + onSave, + parent, + path, +}: { + content: string; + onChange: (content: string) => void; + onSave: () => void; + parent: HTMLElement; + path: string; +}): () => void { + const view = new EditorView({ + parent, + state: EditorState.create({ + doc: content, + extensions: [ + lineNumbers(), + highlightActiveLineGutter(), + history(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + bracketMatching(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + highlightSelectionMatches(), + highlightActiveLine(), + languageForPath(path), + keymap.of([ + { key: 'Mod-s', run: () => { onSave(); return true; } }, + indentWithTab, + ...defaultKeymap, + ...historyKeymap, + ...searchKeymap, + ]), + EditorView.lineWrapping, + EditorView.updateListener.of((update) => { + if (update.docChanged) onChange(update.state.doc.toString()); + }), + EditorView.theme({ + '&': { height: '100%', backgroundColor: '#0c161d', color: '#dbe8ee' }, + '.cm-content': { caretColor: '#73e2c4', padding: '18px 0 40px' }, + '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#73e2c4' }, + '.cm-gutters': { + backgroundColor: '#0c161d', + borderRight: '1px solid rgba(149, 180, 190, .1)', + color: '#49616d', + }, + '.cm-activeLine': { backgroundColor: 'rgba(110, 221, 193, .045)' }, + '.cm-activeLineGutter': { backgroundColor: 'rgba(110, 221, 193, .07)', color: '#88a5af' }, + '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { + backgroundColor: 'rgba(69, 141, 168, .35)', + }, + '.cm-scroller': { fontFamily: 'var(--font-code)', fontSize: '12.5px', lineHeight: '1.68' }, + }), + ], + }), + }); + view.focus(); + return () => view.destroy(); +} diff --git a/apps/web/components/link.tsx b/apps/web/components/link.tsx index 65a5b841f..fc5e02b73 100644 --- a/apps/web/components/link.tsx +++ b/apps/web/components/link.tsx @@ -1,5 +1,5 @@ import NextLink from 'next/link'; -import type { AnchorHTMLAttributes, ReactElement, ReactNode } from 'react'; +import type { AnchorHTMLAttributes, ReactNode } from 'react'; import { Icon } from '@/components/icon'; import { cn } from '@/lib/utils'; @@ -27,7 +27,7 @@ export function Link({ target, variant, ...anchorProps -}: LinkProps): ReactElement { +}: LinkProps): React.ReactElement { const external = /^[a-z][a-z0-9+.-]*:/i.test(href); const opensInNewTab = href.startsWith('http://') || href.startsWith('https://'); const classNames = cn( diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index d5e75fff2..d6d675730 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -12,6 +12,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", + "preserveSymlinks": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", @@ -19,18 +20,6 @@ "paths": { "@/*": [ "./*" - ], - "react": [ - "./node_modules/@types/react" - ], - "react/*": [ - "./node_modules/@types/react/*" - ], - "react-dom": [ - "./node_modules/@types/react-dom" - ], - "react-dom/*": [ - "./node_modules/@types/react-dom/*" ] }, "plugins": [ From fd41c22facadb3be65d96a626a80fe6313581d22 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 14:12:00 -0700 Subject: [PATCH 06/25] chore(lint): classify module declarations --- eslint.config.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9a047907e..911eade37 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,6 +25,10 @@ export default defineConfig( files: ['**/*.{js,mjs,cjs}'], ...tseslint.configs.disableTypeChecked, }, + { + files: ['**/*.d.{ts,mts,cts}'], + ...tseslint.configs.disableTypeChecked, + }, { files: ['**/*.{ts,tsx}'], languageOptions: { From 25dc89c46437ac9f35c6bb1477513bc83ef4539d Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 14:18:05 -0700 Subject: [PATCH 07/25] fix(desktop): make runtime staging noninteractive --- apps/desktop/scripts/stage-sidecar.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/scripts/stage-sidecar.mjs b/apps/desktop/scripts/stage-sidecar.mjs index ab2c24bd0..39bb11e82 100644 --- a/apps/desktop/scripts/stage-sidecar.mjs +++ b/apps/desktop/scripts/stage-sidecar.mjs @@ -100,7 +100,11 @@ execFileSync('pnpm', [ ], { cwd: repoRoot, stdio: 'inherit' }); // Hoisted deploy can rewrite shared workspace linker metadata. Restore the // checked-in lockfile layout before any repository build or test continues. -execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: repoRoot, stdio: 'inherit' }); +execFileSync('pnpm', ['install', '--frozen-lockfile'], { + cwd: repoRoot, + env: { ...process.env, CI: 'true' }, + stdio: 'inherit', +}); const pruned = pruneDeployedRuntime(path.join(runtimeRoot, 'core'), target); process.stdout.write(`Pruned ${pruned.directories} development directories and ${pruned.files} development files.\n`); copyFileSync(path.join(appRoot, 'scripts', 'core-sidecar.mjs'), path.join(runtimeRoot, 'sidecar.mjs')); From 17fe1442254fca3314b3dc50168659d283969d90 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 15:36:23 -0700 Subject: [PATCH 08/25] refactor(graph): share cross-interface visual semantics --- .gitignore | 1 + CONTEXT.md | 1 + apps/desktop/package.json | 5 +- apps/desktop/scripts/stage-material-icons.mjs | 49 +++ apps/desktop/src-tauri/tauri.conf.json | 4 +- apps/desktop/src/materialIconTheme.test.ts | 117 +++++++ apps/desktop/src/materialIconTheme.ts | 235 +++++++++++++ packages/extension/package.json | 1 + .../send/definitions/mergeNodeTypes.ts | 2 +- .../controls/send/definitions/values.ts | 2 +- .../defaults/materialTheme/extensionMatch.ts | 56 --- .../extensionMatch/candidates.ts | 35 -- .../defaults/materialTheme/fileExtension.ts | 14 - .../groups/defaults/materialTheme/fileName.ts | 14 - .../groups/defaults/materialTheme/files.ts | 9 +- .../defaults/materialTheme/folderName.ts | 27 -- .../groups/defaults/materialTheme/folders.ts | 9 +- .../groups/defaults/materialTheme/groups.ts | 8 +- .../groups/defaults/materialTheme/icons.ts | 6 +- .../materialTheme/languageFallback.ts | 31 -- .../materialTheme/languageFallbacks.json | 30 -- .../groups/defaults/materialTheme/manifest.ts | 9 +- .../groups/defaults/materialTheme/match.ts | 81 ----- .../groups/defaults/materialTheme/model.ts | 32 +- .../defaults/materialTheme/pathMatch.ts | 101 ------ .../defaults/materialTheme/pathMatcher.ts | 45 --- .../groups/defaults/materialTheme/paths.ts | 9 - .../groups/defaults/materialTheme/schema.ts | 2 +- .../groups/defaults/materialTheme/svg.ts | 45 --- .../groups/defaults/materialTheme/view.ts | 2 +- .../groups/defaults/symbols/icons.ts | 2 +- .../extension/graphView/settings/reader.ts | 3 +- .../webview/settingsMessages/direction.ts | 2 +- .../src/extension/repoSettings/defaults.ts | 2 +- .../src/shared/fileColorConstants.ts | 32 -- packages/extension/src/shared/fileColors.ts | 37 -- .../defaults/nodeTypes/structural.ts | 2 +- .../extension/src/shared/mock/transforms.ts | 2 +- .../extension/src/shared/normalizeHexColor.ts | 6 + .../webview/app/shell/visibleGraphResponse.ts | 2 +- .../webview/components/graph/model/build.ts | 1 - .../components/graph/model/node/appearance.ts | 3 +- .../components/graph/model/node/display.ts | 5 +- .../components/graph/model/node/sizing.ts | 2 +- .../components/graph/model/node/styleBuild.ts | 10 +- .../graph/model/sizing/calculations.ts | 36 -- .../components/graph/model/sizing/fileSize.ts | 2 +- .../graph/rendering/link/colors/model.ts | 2 +- .../graph/rendering/link/metrics.ts | 16 +- .../components/graph/rendering/node/label.ts | 3 +- .../graph/rendering/node/labelSpriteRaster.ts | 21 +- .../components/graph/rendering/node/media.ts | 5 +- .../graph/rendering/nodes/canvasStyle.ts | 7 +- .../owned2d/layout/collision/radius.ts | 3 +- .../surface/owned2d/layout/runtime/data.ts | 3 +- .../components/graph/runtime/tooltip/rect.ts | 6 +- .../runtime/use/indicators/nodeAppearance.ts | 2 +- .../src/webview/export/json/build/nodes.ts | 2 +- .../src/webview/export/shared/context.ts | 2 +- .../webview/export/svg/link/element/build.ts | 2 +- .../webview/export/svg/node/image/overlay.ts | 3 +- .../webview/graphControls/filtering/nodes.ts | 2 +- .../filtering/rules/nodeLegend/apply.ts | 2 +- .../src/webview/store/initialState.ts | 2 +- .../controls/definitions/definitions.test.ts | 10 +- .../materialTheme/extensionMatch.test.ts | 2 +- .../materialTheme/fileExtension.test.ts | 2 +- .../defaults/materialTheme/fileName.test.ts | 2 +- .../defaults/materialTheme/files.test.ts | 2 +- .../defaults/materialTheme/folderName.test.ts | 2 +- .../defaults/materialTheme/folders.test.ts | 2 +- .../materialTheme/languageFallback.test.ts | 2 +- .../defaults/materialTheme/pathMatch.test.ts | 2 +- .../groups/defaults/materialTheme/svg.test.ts | 2 +- .../graphView/settings/reader.test.ts | 2 +- .../settings/snapshotMessages.test.ts | 2 +- .../extension/repoSettings/defaults.test.ts | 2 +- .../extension/tests/shared/fileColors.test.ts | 4 +- .../graphControls/defaults/nodeTypes.test.ts | 2 +- .../extension/tests/shared/mockData.test.ts | 2 +- ...andtorendersthegraphwitheffective.test.tsx | 2 +- ...atewhentodoesnotrendertheembedded.test.tsx | 2 +- .../webview/app/shell/behavior/fixture.tsx | 2 +- .../webview/app/shell/mutations/fixture.tsx | 2 +- .../app/shell/runtime.mutations.test.tsx | 2 +- .../tests/webview/app/shell/view/fixture.ts | 2 +- .../webview/export/shared/context.test.ts | 2 +- .../webview/export/svg/link/document.test.ts | 2 +- .../export/svg/link/element/build.test.ts | 2 +- .../webview/export/svg/link/links.test.ts | 2 +- .../tests/webview/graph/drag.test.tsx | 2 +- .../graph/marqueeSelection/view.test.tsx | 2 +- .../tests/webview/graph/model/index.test.ts | 2 +- .../model/node/display.mutations.test.ts | 2 +- .../model/node/display.mutations2.test.ts | 2 +- .../webview/graph/model/node/display.test.ts | 3 +- .../sizing/calculations.mutations.test.ts | 2 +- .../graph/model/sizing/calculations.test.ts | 2 +- ...rmalizesinvaliddirectioncolorsback.test.ts | 2 +- .../webview/graph/rendering/links.test.ts | 2 +- .../graph/runtime/tooltip/rect.test.ts | 2 +- .../indicators/nodeAppearance.apply.test.ts | 6 +- .../use/indicators/nodeAppearance.fixture.ts | 2 +- .../graphControls/filtering/nodes.test.ts | 6 +- .../tests/webview/search/filtering.test.ts | 2 +- .../webview/search/filtering/rules.test.ts | 2 +- .../search/filtering/rules/nodes.test.ts | 2 +- .../webview/settingsPanel/Drawer.test.tsx | 2 +- .../webview/store/state/defaults.test.ts | 2 +- .../store/state/runtime/graphMessages.test.ts | 2 +- packages/graph-visuals/package.json | 25 ++ packages/graph-visuals/src/graph.ts | 92 +++++ packages/graph-visuals/src/index.ts | 2 + packages/graph-visuals/src/materialTheme.ts | 329 ++++++++++++++++++ packages/graph-visuals/tests/index.test.ts | 123 +++++++ packages/graph-visuals/tsconfig.json | 15 + packages/graph-visuals/vitest.config.ts | 9 + pnpm-lock.yaml | 18 + quality.config.json | 1 - 119 files changed, 1188 insertions(+), 751 deletions(-) create mode 100644 apps/desktop/scripts/stage-material-icons.mjs create mode 100644 apps/desktop/src/materialIconTheme.test.ts create mode 100644 apps/desktop/src/materialIconTheme.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch/candidates.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileExtension.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileName.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/folderName.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallback.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallbacks.json delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/match.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatch.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatcher.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/paths.ts delete mode 100644 packages/extension/src/extension/graphView/groups/defaults/materialTheme/svg.ts delete mode 100644 packages/extension/src/shared/fileColorConstants.ts delete mode 100644 packages/extension/src/shared/fileColors.ts create mode 100644 packages/extension/src/shared/normalizeHexColor.ts delete mode 100644 packages/extension/src/webview/components/graph/model/sizing/calculations.ts create mode 100644 packages/graph-visuals/package.json create mode 100644 packages/graph-visuals/src/graph.ts create mode 100644 packages/graph-visuals/src/index.ts create mode 100644 packages/graph-visuals/src/materialTheme.ts create mode 100644 packages/graph-visuals/tests/index.test.ts create mode 100644 packages/graph-visuals/tsconfig.json create mode 100644 packages/graph-visuals/vitest.config.ts diff --git a/.gitignore b/.gitignore index 46217296d..b4c941d06 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ apps/desktop/src-tauri/binaries/* apps/desktop/src-tauri/gen/ apps/desktop/src-tauri/runtime/ apps/desktop/src-tauri/target/ +apps/desktop/public/material-icons/ .worktrees/ .playwright-mcp/ .playwright-cli/ diff --git a/CONTEXT.md b/CONTEXT.md index f6ce3edd9..4777e16ce 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -191,6 +191,7 @@ Extension chrome inherits the active VS Code theme. Graph Data Color may encode - `packages/extension` owns the VS Code product surface. - `packages/tldraw` owns the tldraw offline product surface and launcher. - `packages/graph-renderer` owns graph drawing and physics. +- `packages/graph-visuals` owns deterministic visual semantics shared by interfaces, including Material Icon Theme matching, graph sizing metrics, icon geometry, and renderer input defaults. Each interface still owns and resolves its host theme colors. - `packages/plugin-api` owns public Core plugin contracts. - `packages/extension-plugin-api` owns public VS Code Extension plugin contracts. - `packages/plugin-*` own optional Core or interface plugins. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c59520be6..f7d4b8e3c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,9 +8,10 @@ "node": "^22.14.0 || >=23.6.0" }, "scripts": { - "build": "vite build", + "build": "pnpm run build:icons && vite build", "build:bundle": "tauri build", "build:bundle:ad-hoc": "tauri build --target aarch64-apple-darwin --bundles app,dmg --config src-tauri/tauri.ad-hoc.conf.json", + "build:icons": "node scripts/stage-material-icons.mjs", "build:sidecar": "node scripts/stage-sidecar.mjs", "check:bundle": "node scripts/verify-release-bundle.mjs", "dev": "tauri dev --no-watch", @@ -26,9 +27,11 @@ "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.8", "@codegraphy-dev/core": "workspace:*", + "@codegraphy-dev/graph-visuals": "workspace:*", "@codegraphy-dev/graph-renderer": "workspace:*", "@lezer/highlight": "^1.2.3", "@tauri-apps/api": "^2.11.1", + "material-icon-theme": "5.33.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/apps/desktop/scripts/stage-material-icons.mjs b/apps/desktop/scripts/stage-material-icons.mjs new file mode 100644 index 000000000..42b79c79b --- /dev/null +++ b/apps/desktop/scripts/stage-material-icons.mjs @@ -0,0 +1,49 @@ +import { cp, copyFile, mkdir, readdir, rm, stat } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const desktopRoot = resolve(scriptDirectory, '..'); +const packageRoot = dirname(require.resolve('material-icon-theme/package.json')); +const sourceManifest = join(packageRoot, 'dist', 'material-icons.json'); +const sourceIcons = join(packageRoot, 'icons'); +const stagingRoot = join(desktopRoot, 'public', 'material-icons'); +const stagedManifest = join(stagingRoot, 'dist', 'material-icons.json'); +const stagedIcons = join(stagingRoot, 'icons'); + +await rm(stagingRoot, { force: true, recursive: true }); +await mkdir(dirname(stagedManifest), { recursive: true }); +await copyFile(sourceManifest, stagedManifest); +await cp(sourceIcons, stagedIcons, { recursive: true }); + +const staged = await measureFiles(stagingRoot); +console.log(`Staged Material Icon Theme: ${staged.count} files, ${formatBytes(staged.bytes)}.`); + +async function measureFiles(root) { + const entries = (await readdir(root, { withFileTypes: true })) + .sort((left, right) => left.name.localeCompare(right.name)); + let count = 0; + let bytes = 0; + + for (const entry of entries) { + const entryPath = join(root, entry.name); + if (entry.isDirectory()) { + const nested = await measureFiles(entryPath); + count += nested.count; + bytes += nested.bytes; + } else if (entry.isFile()) { + count += 1; + bytes += (await stat(entryPath)).size; + } + } + + return { count, bytes }; +} + +function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 06b702a2b..81bd1913c 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ "version": "0.1.0", "identifier": "dev.codegraphy.desktop", "build": { - "beforeDevCommand": "pnpm run build:sidecar && pnpm exec vite", + "beforeDevCommand": "pnpm run build:sidecar && pnpm run build:icons && pnpm exec vite", "beforeBuildCommand": "pnpm run build:sidecar && pnpm run build", "devUrl": "http://localhost:1420", "frontendDist": "../dist" @@ -33,7 +33,7 @@ "copyright": "Copyright © 2026 Joe Soboleski", "externalBin": ["binaries/codegraphy-core"], "icon": ["icons/icon.icns"], - "longDescription": "A fast local code navigator for Files, Symbols, and their Relationships.", + "longDescription": "A fast local code navigator for Files, Folders, and their Relationships.", "macOS": { "entitlements": "Entitlements.plist", "hardenedRuntime": true, diff --git a/apps/desktop/src/materialIconTheme.test.ts b/apps/desktop/src/materialIconTheme.test.ts new file mode 100644 index 000000000..13a81b4f5 --- /dev/null +++ b/apps/desktop/src/materialIconTheme.test.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + clearMaterialIconThemeCache, + resolveMaterialIcon, +} from './materialIconTheme'; + +const manifest = { + fileExtensions: { ts: 'typescript' }, + folder: 'folder', + folderNames: { src: 'folder-src' }, + iconDefinitions: { + typescript: { iconPath: './../icons/typescript.svg' }, + folder: { iconPath: './../icons/folder.svg' }, + 'folder-src': { iconPath: './../icons/folder-src.svg' }, + }, +}; + +describe('Material Icon Theme adapter', () => { + beforeEach(() => { + clearMaterialIconThemeCache(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('resolves and caches a white File icon with its original primary color', async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = getRequestUrl(input); + return url.endsWith('material-icons.json') + ? Response.json(manifest) + : new Response(''); + }); + vi.stubGlobal('fetch', fetchMock); + + const first = await resolveMaterialIcon('src/index.ts', 'file'); + const second = await resolveMaterialIcon('src/index.ts', 'file'); + const sameIconForAnotherPath = await resolveMaterialIcon('src/model.ts', 'file'); + + expect(first?.color).toBe('#123456'); + expect(first?.imageUrl).toMatch(/^data:image\/svg\+xml;base64,/); + expect(first?.mode).toBe('file'); + expect(decodeSvg(first?.imageUrl)).toContain('#FFFFFF'); + expect(decodeSvg(first?.imageUrl)).not.toContain('#123456'); + expect(second).toBe(first); + expect(sameIconForAnotherPath).toBe(first); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('keeps Folder SVG colors and requests transparent node semantics', async () => { + vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => + getRequestUrl(input).endsWith('material-icons.json') + ? Response.json(manifest) + : new Response(''))); + + const icon = await resolveMaterialIcon('src', 'folder'); + + expect(icon?.color).toBe('rgba(0, 0, 0, 0)'); + expect(icon?.mode).toBe('folder'); + expect(decodeSvg(icon?.imageUrl)).toContain('#ABCDEF'); + }); + + it('uses the generic Folder icon when no named Folder rule matches', async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => + getRequestUrl(input).endsWith('material-icons.json') + ? Response.json(manifest) + : new Response('')); + vi.stubGlobal('fetch', fetchMock); + + const icon = await resolveMaterialIcon('docs', 'folder'); + + expect(icon?.color).toBe('rgba(0, 0, 0, 0)'); + expect(decodeSvg(icon?.imageUrl)).toContain('#FEDCBA'); + expect(getRequestUrl(fetchMock.mock.calls[1]?.[0] ?? '')).toMatch(/\/icons\/folder\.svg$/); + }); + + it('returns undefined for a missing match and caches that work', async () => { + const fetchMock = vi.fn(async () => Response.json(manifest)); + vi.stubGlobal('fetch', fetchMock); + + await expect(resolveMaterialIcon('README.unknown', 'file')).resolves.toBeUndefined(); + await expect(resolveMaterialIcon('README.unknown', 'file')).resolves.toBeUndefined(); + expect(fetchMock).toHaveBeenCalledOnce(); + }); + + it('rejects malformed manifest fields at the fetch boundary', async () => { + vi.stubGlobal('fetch', vi.fn(async () => Response.json({ fileExtensions: ['ts'] }))); + + await expect(resolveMaterialIcon('src/index.ts', 'file')).rejects.toThrow( + 'Material Icon Theme manifest field "fileExtensions" must be an object.', + ); + }); + + it('rejects icon paths that escape the staged asset root', async () => { + vi.stubGlobal('fetch', vi.fn(async () => Response.json({ + fileExtensions: { ts: 'typescript' }, + iconDefinitions: { typescript: { iconPath: '../../../outside.svg' } }, + }))); + + await expect(resolveMaterialIcon('src/index.ts', 'file')).rejects.toThrow( + 'Material Icon Theme manifest contains an icon path outside staged assets.', + ); + }); +}); + +function decodeSvg(imageUrl: string | undefined): string { + if (!imageUrl) throw new Error('Expected an SVG data URL.'); + const encoded = imageUrl.split(',')[1]; + if (!encoded) throw new Error('Expected base64 SVG data.'); + const bytes = Uint8Array.from(atob(encoded), character => character.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +function getRequestUrl(input: RequestInfo | URL): string { + if (typeof input === 'string') return input; + return input instanceof URL ? input.href : input.url; +} diff --git a/apps/desktop/src/materialIconTheme.ts b/apps/desktop/src/materialIconTheme.ts new file mode 100644 index 000000000..91daadc7f --- /dev/null +++ b/apps/desktop/src/materialIconTheme.ts @@ -0,0 +1,235 @@ +import { + createMaterialExtensionMatcher, + createMaterialPathRuleMatcher, + extractPrimaryColor, + findMaterialMatch, + MATERIAL_TRANSPARENT_NODE_COLOR, + toSvgDataUrl, + toWhiteSvg, +} from '@codegraphy-dev/graph-visuals'; +import type { MaterialIconManifest } from '@codegraphy-dev/graph-visuals'; + +const MATERIAL_MANIFEST_URL = './material-icons/dist/material-icons.json'; +const MATERIAL_ASSET_ROOT = './material-icons/'; + +type MaterialIconMode = 'file' | 'folder'; + +type MaterialIconDefinition = NonNullable[string]; + +interface MaterialTheme { + extensionMatcher?: ReturnType; + manifest: MaterialIconManifest; + pathMatchers: { + fileNames?: ReturnType; + folderNames?: ReturnType; + folderNamesExpanded?: ReturnType; + }; +} + +export interface MaterialIconData { + color: string; + imageUrl: string; + mode: MaterialIconMode; +} + +let themePromise: Promise | undefined; +const iconCache = new Map>(); +const matchCache = new Map>(); + +export function clearMaterialIconThemeCache(): void { + themePromise = undefined; + iconCache.clear(); + matchCache.clear(); +} + +export function resolveMaterialIcon( + nodePath: string, + mode: MaterialIconMode, +): Promise { + const cacheKey = `${mode}:${nodePath}`; + const cached = matchCache.get(cacheKey); + if (cached) return cached; + + const resolution = resolveUncachedMaterialIcon(nodePath, mode); + matchCache.set(cacheKey, resolution); + return resolution; +} + +async function resolveUncachedMaterialIcon( + nodePath: string, + mode: MaterialIconMode, +): Promise { + const theme = await loadMaterialTheme(); + const match = findMaterialMatch(nodePath, theme.manifest, { + extensionMatcher: theme.extensionMatcher, + nodeType: mode, + pathMatchers: theme.pathMatchers, + }); + const iconName = match?.iconName ?? (mode === 'folder' ? theme.manifest.folder : undefined); + if (!iconName) return undefined; + + const cacheKey = `${mode}:${iconName}`; + const cached = iconCache.get(cacheKey); + if (cached) return cached; + + const resolution = resolveMatchedIcon(theme.manifest, iconName, mode); + iconCache.set(cacheKey, resolution); + return resolution; +} + +async function resolveMatchedIcon( + manifest: MaterialIconManifest, + iconName: string, + mode: MaterialIconMode, +): Promise { + const iconPath = manifest.iconDefinitions?.[iconName]?.iconPath; + if (!iconPath) return undefined; + + const iconUrl = resolveIconUrl(iconPath); + const response = await fetch(iconUrl); + if (!response.ok) { + throw new Error(`Material icon request failed with status ${response.status}.`); + } + + const svg = await response.text(); + return mode === 'folder' + ? { color: MATERIAL_TRANSPARENT_NODE_COLOR, imageUrl: toSvgDataUrl(svg), mode } + : { + color: extractPrimaryColor(svg), + imageUrl: toSvgDataUrl(toWhiteSvg(svg)), + mode, + }; +} + +function loadMaterialTheme(): Promise { + themePromise ??= loadUncachedMaterialTheme(); + return themePromise; +} + +async function loadUncachedMaterialTheme(): Promise { + const response = await fetch(MATERIAL_MANIFEST_URL); + if (!response.ok) { + throw new Error(`Material Icon Theme manifest request failed with status ${response.status}.`); + } + + const manifest = parseMaterialIconManifest(await response.json()); + return { + extensionMatcher: manifest.fileExtensions + ? createMaterialExtensionMatcher(manifest.fileExtensions) + : undefined, + manifest, + pathMatchers: { + fileNames: manifest.fileNames + ? createMaterialPathRuleMatcher(manifest.fileNames) + : undefined, + folderNames: manifest.folderNames + ? createMaterialPathRuleMatcher(manifest.folderNames) + : undefined, + folderNamesExpanded: manifest.folderNamesExpanded + ? createMaterialPathRuleMatcher(manifest.folderNamesExpanded) + : undefined, + }, + }; +} + +function parseMaterialIconManifest(value: unknown): MaterialIconManifest { + if (!isRecord(value)) { + throw new Error('Material Icon Theme manifest must be an object.'); + } + + return compactManifest({ + fileExtensions: parseOptionalStringRecord(value, 'fileExtensions'), + fileNames: parseOptionalStringRecord(value, 'fileNames'), + folder: parseOptionalString(value, 'folder'), + folderNames: parseOptionalStringRecord(value, 'folderNames'), + folderNamesExpanded: parseOptionalStringRecord(value, 'folderNamesExpanded'), + iconDefinitions: parseOptionalIconDefinitions(value.iconDefinitions), + languageIds: parseOptionalStringRecord(value, 'languageIds'), + rootFolder: parseOptionalString(value, 'rootFolder'), + }); +} + +function compactManifest(manifest: MaterialIconManifest): MaterialIconManifest { + return { + ...(manifest.fileExtensions !== undefined + ? { fileExtensions: manifest.fileExtensions } + : {}), + ...(manifest.fileNames !== undefined ? { fileNames: manifest.fileNames } : {}), + ...(manifest.folder !== undefined ? { folder: manifest.folder } : {}), + ...(manifest.folderNames !== undefined ? { folderNames: manifest.folderNames } : {}), + ...(manifest.folderNamesExpanded !== undefined + ? { folderNamesExpanded: manifest.folderNamesExpanded } + : {}), + ...(manifest.iconDefinitions !== undefined + ? { iconDefinitions: manifest.iconDefinitions } + : {}), + ...(manifest.languageIds !== undefined ? { languageIds: manifest.languageIds } : {}), + ...(manifest.rootFolder !== undefined ? { rootFolder: manifest.rootFolder } : {}), + }; +} + +function parseOptionalString( + record: Record, + key: string, +): string | undefined { + const value = record[key]; + if (value === undefined) return undefined; + if (typeof value !== 'string') { + throw new Error(`Material Icon Theme manifest field "${key}" must be a string.`); + } + return value; +} + +function parseOptionalStringRecord( + record: Record, + key: string, +): Record | undefined { + const value = record[key]; + if (value === undefined) return undefined; + if (!isRecord(value)) { + throw new Error(`Material Icon Theme manifest field "${key}" must be an object.`); + } + + const parsed: Record = {}; + for (const [entryKey, entryValue] of Object.entries(value)) { + if (typeof entryValue !== 'string') { + throw new Error(`Material Icon Theme manifest field "${key}.${entryKey}" must be a string.`); + } + parsed[entryKey] = entryValue; + } + return parsed; +} + +function parseOptionalIconDefinitions( + value: unknown, +): Record | undefined { + if (value === undefined) return undefined; + if (!isRecord(value)) { + throw new Error('Material Icon Theme manifest field "iconDefinitions" must be an object.'); + } + + const definitions: Record = {}; + for (const [iconName, definition] of Object.entries(value)) { + if (!isRecord(definition) || typeof definition.iconPath !== 'string') { + throw new Error( + `Material Icon Theme manifest field "iconDefinitions.${iconName}.iconPath" must be a string.`, + ); + } + definitions[iconName] = { iconPath: definition.iconPath }; + } + return definitions; +} + +function resolveIconUrl(iconPath: string): string { + const assetRoot = new URL(MATERIAL_ASSET_ROOT, document.baseURI); + const manifestUrl = new URL(MATERIAL_MANIFEST_URL, document.baseURI); + const iconUrl = new URL(iconPath, manifestUrl); + if (iconUrl.origin !== assetRoot.origin || !iconUrl.pathname.startsWith(assetRoot.pathname)) { + throw new Error('Material Icon Theme manifest contains an icon path outside staged assets.'); + } + return iconUrl.href; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/extension/package.json b/packages/extension/package.json index cbd489ed6..1de4628f2 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -33,6 +33,7 @@ "@codegraphy-dev/core": "workspace:*", "@codegraphy-dev/extension-plugin-api": "workspace:*", "@codegraphy-dev/graph-renderer": "workspace:*", + "@codegraphy-dev/graph-visuals": "workspace:*", "@codegraphy-dev/plugin-api": "workspace:*", "@driftlog/tree-sitter-dart": "1.0.4", "@floating-ui/react": "^0.27.19", diff --git a/packages/extension/src/extension/graphView/controls/send/definitions/mergeNodeTypes.ts b/packages/extension/src/extension/graphView/controls/send/definitions/mergeNodeTypes.ts index fc3b88d4b..a6292611f 100644 --- a/packages/extension/src/extension/graphView/controls/send/definitions/mergeNodeTypes.ts +++ b/packages/extension/src/extension/graphView/controls/send/definitions/mergeNodeTypes.ts @@ -1,7 +1,7 @@ import type { IGraphData } from '../../../../../shared/graph/contracts'; import type { IGraphNodeTypeDefinition } from '../../../../../shared/graphControls/contracts'; import { CORE_GRAPH_NODE_TYPES } from '../../../../../shared/graphControls/defaults/definitions'; -import { normalizeHexColor } from '../../../../../shared/fileColors'; +import { normalizeHexColor } from '../../../../../shared/normalizeHexColor'; import type { GraphNodeTypeCapabilityLike, GraphNodeTypeLike } from './contracts'; const STRUCTURAL_NODE_TYPE_IDS = new Set(['file', 'folder', 'package']); diff --git a/packages/extension/src/extension/graphView/controls/send/definitions/values.ts b/packages/extension/src/extension/graphView/controls/send/definitions/values.ts index c5048b73d..29ee931b9 100644 --- a/packages/extension/src/extension/graphView/controls/send/definitions/values.ts +++ b/packages/extension/src/extension/graphView/controls/send/definitions/values.ts @@ -1,7 +1,7 @@ import type { IGraphNodeTypeDefinition, } from '../../../../../shared/graphControls/contracts'; -import { normalizeHexColor } from '../../../../../shared/fileColors'; +import { normalizeHexColor } from '../../../../../shared/normalizeHexColor'; export function resolveVisibilityMap( definitions: TDefinition[], diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch.ts deleted file mode 100644 index da1ec9e48..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { MaterialMatch } from './model'; -import { - createExtensionMatch, - getExtensionCandidates, -} from './extensionMatch/candidates'; - -export interface MaterialExtensionMatcher { - iconNameByLowerExtension: Map; -} - -export function createMaterialExtensionMatcher( - extensions: Record, -): MaterialExtensionMatcher { - return { - iconNameByLowerExtension: new Map( - Object.entries(extensions).map(([extension, iconName]) => [ - extension.toLowerCase(), - iconName, - ]), - ), - }; -} - -export function findLongestExtensionMatch( - baseName: string, - entries: Iterable, -): MaterialMatch | undefined { - return findLongestExtensionMatchWithMatcher( - baseName, - createMaterialExtensionMatcher(Object.fromEntries(entries)), - ); -} - -export function findLongestExtensionMatchWithMatcher( - baseName: string, - matcher: MaterialExtensionMatcher, -): MaterialMatch | undefined { - const lowerBaseName = baseName.toLowerCase(); - let bestMatch: MaterialMatch | undefined; - - for (const extension of getExtensionCandidates(lowerBaseName)) { - const iconName = matcher.iconNameByLowerExtension.get(extension); - if (!iconName) { - continue; - } - - const match = createExtensionMatch(baseName, lowerBaseName, extension, iconName); - if (!match || (bestMatch && bestMatch.key.length >= match.key.length)) { - continue; - } - - bestMatch = match; - } - - return bestMatch; -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch/candidates.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch/candidates.ts deleted file mode 100644 index 2ee03c6be..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/extensionMatch/candidates.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { MaterialMatch } from '../model'; - -export function getExtensionCandidates(lowerBaseName: string): string[] { - const candidates = [lowerBaseName]; - for (let index = lowerBaseName.indexOf('.'); index >= 0; index = lowerBaseName.indexOf('.', index + 1)) { - const extension = lowerBaseName.slice(index + 1); - if (extension) { - candidates.push(extension); - } - } - - return candidates; -} - -export function createExtensionMatch( - baseName: string, - lowerBaseName: string, - extension: string, - iconName: string, -): MaterialMatch | undefined { - const lowerExtension = extension.toLowerCase(); - if (!matchesExtension(lowerBaseName, lowerExtension)) { - return undefined; - } - - return { - iconName, - key: lowerBaseName === lowerExtension ? baseName : baseName.slice(-extension.length), - kind: 'fileExtension', - }; -} - -function matchesExtension(lowerBaseName: string, lowerExtension: string): boolean { - return lowerBaseName === lowerExtension || lowerBaseName.endsWith(`.${lowerExtension}`); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileExtension.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileExtension.ts deleted file mode 100644 index d67771ba1..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileExtension.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MaterialMatch } from './model'; -import { - createMaterialExtensionMatcher, - findLongestExtensionMatchWithMatcher, - type MaterialExtensionMatcher, -} from './extensionMatch'; - -export function matchMaterialFileExtension( - baseName: string, - fileExtensions: Record, - matcher: MaterialExtensionMatcher = createMaterialExtensionMatcher(fileExtensions), -): MaterialMatch | undefined { - return findLongestExtensionMatchWithMatcher(baseName, matcher); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileName.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileName.ts deleted file mode 100644 index 2228b59ba..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/fileName.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MaterialMatch } from './model'; -import { - createMaterialPathRuleMatcher, - findLongestPathMatchWithMatcher, - type MaterialPathRuleMatcher, -} from './pathMatch'; - -export function matchMaterialFileName( - nodeId: string, - fileNames: Record, - matcher: MaterialPathRuleMatcher = createMaterialPathRuleMatcher(fileNames), -): MaterialMatch | undefined { - return findLongestPathMatchWithMatcher(nodeId, matcher, 'fileName'); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts index 05f327e5d..972dcd9fe 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts @@ -1,11 +1,14 @@ import type { IGroup } from '../../../../../shared/settings/groups'; import type { IGraphData } from '../../../../../shared/graph/contracts'; +import { + findMaterialMatch, + getMaterialBaseName, + type MaterialMatch, +} from '@codegraphy-dev/graph-visuals'; import { isExternalPackageNodeId } from '../../../../pipeline/graph/packageSpecifiers/nodeId'; -import type { MaterialMatch, MaterialThemeCacheEntry } from './model'; +import type { MaterialThemeCacheEntry } from './model'; import { createMaterialGroup } from './groups'; import { resolveIconData } from './icons'; -import { findMaterialMatch } from './match'; -import { getMaterialBaseName } from './paths'; function hasPathSpecificFileNameRules( baseName: string, diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folderName.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folderName.ts deleted file mode 100644 index 09b30bc7a..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folderName.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { MaterialMatch } from './model'; -import { - createMaterialPathRuleMatcher, - findLongestPathMatchWithMatcher, - type MaterialPathRuleMatcher, -} from './pathMatch'; - -export function matchMaterialFolderName( - folderPath: string, - folderNames: Record, - folderNamesExpanded: Record = {}, - matchers: { - folderNames?: MaterialPathRuleMatcher; - folderNamesExpanded?: MaterialPathRuleMatcher; - } = {}, -): MaterialMatch | undefined { - return findLongestPathMatchWithMatcher( - folderPath, - matchers.folderNames ?? createMaterialPathRuleMatcher(folderNames), - 'folderName', - ) - ?? findLongestPathMatchWithMatcher( - folderPath, - matchers.folderNamesExpanded ?? createMaterialPathRuleMatcher(folderNamesExpanded), - 'folderName', - ); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts index 18e0fac87..39ec37038 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts @@ -1,16 +1,19 @@ import type { IGroup } from '../../../../../shared/settings/groups'; import type { IGraphData } from '../../../../../shared/graph/contracts'; +import { + findMaterialMatch, + MATERIAL_TRANSPARENT_NODE_COLOR, + type MaterialIconManifest, + type MaterialMatch, +} from '@codegraphy-dev/graph-visuals'; import { collectFolderPaths } from '../../../../../shared/graphControls/nests/folders'; import { isExternalPackageNodeId } from '../../../../pipeline/graph/packageSpecifiers/nodeId'; import type { MaterialThemeCacheEntry } from './model'; import { createGenericFolderGroup, createMaterialGroup, - MATERIAL_TRANSPARENT_NODE_COLOR, } from './groups'; import { resolveIconData } from './icons'; -import { findMaterialMatch } from './match'; -import type { MaterialIconManifest, MaterialMatch } from './model'; function getDefaultFolderIconName( _folderPath: string, diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts index 303485f75..4d97a213a 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts @@ -1,7 +1,9 @@ import type { IGroup } from '../../../../../shared/settings/groups'; -import type { MaterialIconData, MaterialMatch } from './model'; - -export const MATERIAL_TRANSPARENT_NODE_COLOR = 'rgba(0, 0, 0, 0)'; +import { + MATERIAL_TRANSPARENT_NODE_COLOR, + type MaterialMatch, +} from '@codegraphy-dev/graph-visuals'; +import type { MaterialIconData } from './model'; export function createMaterialGroup( match: MaterialMatch, diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts index a37b34c96..02d7884fc 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts @@ -1,7 +1,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import { + extractPrimaryColor, + toSvgDataUrl, + toWhiteSvgDataUrl, +} from '@codegraphy-dev/graph-visuals'; import type { MaterialIconData, MaterialThemeCacheEntry } from './model'; -import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from './svg'; export function resolveIconData( theme: MaterialThemeCacheEntry, diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallback.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallback.ts deleted file mode 100644 index c27762ba9..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallback.ts +++ /dev/null @@ -1,31 +0,0 @@ -import languageFallbacks from './languageFallbacks.json'; -import { findLongestExtensionMatch } from './extensionMatch'; -import type { MaterialMatch } from './model'; - -const LANGUAGE_FALLBACKS = languageFallbacks as Array<{ extensions: string[]; languageId: string }>; - -export function matchMaterialLanguageFallback( - baseName: string, - languageIds: Record, -): MaterialMatch | undefined { - return findLongestExtensionMatch(baseName, getLanguageFallbackEntries(languageIds)); -} - -function getLanguageFallbackEntries( - languageIds: Record, -): Array { - const entries: Array = []; - - for (const { extensions, languageId } of LANGUAGE_FALLBACKS) { - const iconName = languageIds[languageId]; - if (!iconName) { - continue; - } - - for (const extension of extensions) { - entries.push([extension, iconName]); - } - } - - return entries; -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallbacks.json b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallbacks.json deleted file mode 100644 index 782d73f7f..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/languageFallbacks.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { "languageId": "javascript", "extensions": ["js", "cjs", "mjs", "esx"] }, - { "languageId": "typescript", "extensions": ["ts", "cts", "mts", "ets"] }, - { "languageId": "javascriptreact", "extensions": ["jsx"] }, - { "languageId": "typescriptreact", "extensions": ["tsx"] }, - { "languageId": "markdown", "extensions": ["markdown", "md", "rst"] }, - { "languageId": "python", "extensions": ["py", "pyi"] }, - { "languageId": "java", "extensions": ["java"] }, - { "languageId": "csharp", "extensions": ["cs"] }, - { "languageId": "rust", "extensions": ["rs"] }, - { "languageId": "go", "extensions": ["go"] }, - { "languageId": "vue", "extensions": ["vue"] }, - { "languageId": "html", "extensions": ["html", "htm", "xhtml"] }, - { "languageId": "css", "extensions": ["css"] }, - { "languageId": "sass", "extensions": ["scss", "sass"] }, - { "languageId": "less", "extensions": ["less"] }, - { "languageId": "yaml", "extensions": ["yaml", "yml"] }, - { "languageId": "xml", "extensions": ["xml", "plist", "xsd", "dtd", "xsl", "xslt", "resx"] }, - { "languageId": "toml", "extensions": ["toml"] }, - { "languageId": "json", "extensions": ["json", "jsonc", "json5", "jsonl"] }, - { "languageId": "sql", "extensions": ["sql"] }, - { "languageId": "php", "extensions": ["php"] }, - { "languageId": "ruby", "extensions": ["rb"] }, - { "languageId": "lua", "extensions": ["lua"] }, - { "languageId": "swift", "extensions": ["swift"] }, - { "languageId": "dart", "extensions": ["dart"] }, - { "languageId": "svelte", "extensions": ["svelte"] }, - { "languageId": "c", "extensions": ["c", "h"] }, - { "languageId": "cpp", "extensions": ["cc", "cpp", "cxx", "hh", "hpp", "hxx"] } -] diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts index cc67115bd..ef1e6ab72 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts @@ -1,9 +1,12 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as vscode from 'vscode'; -import type { MaterialThemeCacheEntry, MaterialIconManifest } from './model'; -import { createMaterialExtensionMatcher } from './extensionMatch'; -import { createMaterialPathRuleMatcher } from './pathMatch'; +import { + createMaterialExtensionMatcher, + createMaterialPathRuleMatcher, + type MaterialIconManifest, +} from '@codegraphy-dev/graph-visuals'; +import type { MaterialThemeCacheEntry } from './model'; import { materialIconManifestSchema } from './schema'; const materialThemeCache = new Map(); diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/match.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/match.ts deleted file mode 100644 index 2d9eb8074..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/match.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { MaterialExtensionMatcher } from './extensionMatch'; -import type { MaterialIconManifest, MaterialMatch, MaterialThemePathMatchers } from './model'; -import { matchMaterialFileExtension } from './fileExtension'; -import { matchMaterialFileName } from './fileName'; -import { matchMaterialFolderName } from './folderName'; -import { matchMaterialLanguageFallback } from './languageFallback'; -import { getMaterialBaseName } from './paths'; - -export function findMaterialMatch( - nodeId: string, - manifest: MaterialIconManifest, - options?: { - extensionMatcher?: MaterialExtensionMatcher; - nodeType?: 'file' | 'folder'; - pathMatchers?: MaterialThemePathMatchers; - }, -): MaterialMatch | undefined { - return options?.nodeType === 'folder' - ? findFolderMaterialMatch(nodeId, manifest, options.pathMatchers) - : findFileMaterialMatch(nodeId, manifest, options?.pathMatchers, options?.extensionMatcher); -} - -function findFolderMaterialMatch( - nodeId: string, - manifest: MaterialIconManifest, - pathMatchers: MaterialThemePathMatchers | undefined, -): MaterialMatch | undefined { - return manifest.folderNames - ? matchMaterialFolderName( - nodeId, - manifest.folderNames, - manifest.folderNamesExpanded, - pathMatchers, - ) - : undefined; -} - -function findFileMaterialMatch( - nodeId: string, - manifest: MaterialIconManifest, - pathMatchers: MaterialThemePathMatchers | undefined, - extensionMatcher: MaterialExtensionMatcher | undefined, -): MaterialMatch | undefined { - const baseName = getMaterialBaseName(nodeId); - if (!baseName) { - return undefined; - } - - return findFileNameMaterialMatch(nodeId, manifest, pathMatchers) - ?? findFileExtensionMaterialMatch(baseName, manifest, extensionMatcher) - ?? findLanguageMaterialMatch(baseName, manifest); -} - -function findFileNameMaterialMatch( - nodeId: string, - manifest: MaterialIconManifest, - pathMatchers: MaterialThemePathMatchers | undefined, -): MaterialMatch | undefined { - return manifest.fileNames - ? matchMaterialFileName(nodeId, manifest.fileNames, pathMatchers?.fileNames) - : undefined; -} - -function findFileExtensionMaterialMatch( - baseName: string, - manifest: MaterialIconManifest, - extensionMatcher: MaterialExtensionMatcher | undefined, -): MaterialMatch | undefined { - return manifest.fileExtensions - ? matchMaterialFileExtension(baseName, manifest.fileExtensions, extensionMatcher) - : undefined; -} - -function findLanguageMaterialMatch( - baseName: string, - manifest: MaterialIconManifest, -): MaterialMatch | undefined { - return manifest.languageIds - ? matchMaterialLanguageFallback(baseName, manifest.languageIds) - : undefined; -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts index debb4a37d..be198e655 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts @@ -1,16 +1,8 @@ -import type { MaterialExtensionMatcher } from './extensionMatch'; -import type { MaterialPathRuleMatcher } from './pathMatch'; - -export interface MaterialIconManifest { - fileExtensions?: Record; - fileNames?: Record; - folder?: string; - folderNames?: Record; - folderNamesExpanded?: Record; - iconDefinitions?: Record; - languageIds?: Record; - rootFolder?: string; -} +import type { + MaterialExtensionMatcher, + MaterialIconManifest, + MaterialThemePathMatchers, +} from '@codegraphy-dev/graph-visuals'; export interface MaterialIconData { color: string; @@ -24,17 +16,3 @@ export interface MaterialThemeCacheEntry { manifestPath: string; pathMatchers: MaterialThemePathMatchers; } - -export interface MaterialThemePathMatchers { - fileNames?: MaterialPathRuleMatcher; - folderNames?: MaterialPathRuleMatcher; - folderNamesExpanded?: MaterialPathRuleMatcher; -} - -export interface MaterialMatch { - iconName: string; - key: string; - kind: 'fileExtension' | 'fileName' | 'folderName'; -} - -export const DEFAULT_MATERIAL_COLOR = '#90A4AE'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatch.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatch.ts deleted file mode 100644 index c8a44da2d..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatch.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { MaterialMatch } from './model'; -import { getMaterialBaseName, normalizePathSeparators } from './paths'; -import { - createMaterialPathRuleMatcher, - type MaterialPathRuleEntry, - type MaterialPathRuleMatcher, -} from './pathMatcher'; - -export { - createMaterialPathRuleMatcher, - type MaterialPathRuleMatcher, -}; - -type PathMatchKind = Extract; - -interface PathMatchContext { - baseName: string; - lowerBaseName: string; - lowerSubjectPath: string; - subjectPath: string; -} - -export function findLongestPathMatch( - subjectPath: string, - rules: Record, - kind: PathMatchKind, -): MaterialMatch | undefined { - return findLongestPathMatchWithMatcher( - subjectPath, - createMaterialPathRuleMatcher(rules), - kind, - ); -} - -export function findLongestPathMatchWithMatcher( - subjectPath: string, - matcher: MaterialPathRuleMatcher, - kind: PathMatchKind, -): MaterialMatch | undefined { - const context = getPathMatchContext(subjectPath); - for (const rule of matcher.pathRulesByLowerBaseName.get(context.lowerBaseName) ?? []) { - if (!matchesPathRule(context, rule.normalizedRule, rule.lowerRule)) { - continue; - } - - return createMaterialMatch(context, rule, kind); - } - - const baseNameRule = matcher.baseNameRules.get(context.lowerBaseName); - return baseNameRule - ? createMaterialMatch(context, baseNameRule, kind) - : undefined; -} - -function getPathMatchContext(subjectPath: string): PathMatchContext { - const normalizedPath = normalizePathSeparators(subjectPath); - return { - baseName: getMaterialBaseName(subjectPath), - lowerBaseName: getMaterialBaseName(subjectPath).toLowerCase(), - lowerSubjectPath: normalizedPath.toLowerCase(), - subjectPath: normalizedPath, - }; -} - -function createMaterialMatch( - context: PathMatchContext, - rule: MaterialPathRuleEntry, - kind: PathMatchKind, -): MaterialMatch | undefined { - return { - iconName: rule.iconName, - key: resolveMatchedPathKey(context, rule.normalizedRule, rule.lowerRule), - kind, - }; -} - -function matchesPathRule( - context: PathMatchContext, - normalizedRule: string, - lowerRule: string, -): boolean { - if (!normalizedRule.includes('/')) { - return context.lowerBaseName === lowerRule; - } - - return context.lowerSubjectPath === lowerRule || context.lowerSubjectPath.endsWith(`/${lowerRule}`); -} - -function resolveMatchedPathKey( - context: PathMatchContext, - normalizedRule: string, - lowerRule: string, -): string { - if (!normalizedRule.includes('/')) { - return context.baseName; - } - - return context.lowerSubjectPath === lowerRule - ? context.subjectPath - : context.subjectPath.slice(-normalizedRule.length); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatcher.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatcher.ts deleted file mode 100644 index 4fb1b37dc..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/pathMatcher.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { getMaterialBaseName, normalizePathSeparators } from './paths'; - -export interface MaterialPathRuleEntry { - iconName: string; - lowerRule: string; - normalizedRule: string; -} - -export interface MaterialPathRuleMatcher { - baseNameRules: Map; - pathRules: MaterialPathRuleEntry[]; - pathRulesByLowerBaseName: Map; -} - -export function createMaterialPathRuleMatcher( - rules: Record, -): MaterialPathRuleMatcher { - const baseNameRules = new Map(); - const pathRules: MaterialPathRuleEntry[] = []; - const pathRulesByLowerBaseName = new Map(); - - for (const [ruleKey, iconName] of Object.entries(rules)) { - const normalizedRule = normalizePathSeparators(ruleKey); - const lowerRule = normalizedRule.toLowerCase(); - const entry = { iconName, lowerRule, normalizedRule }; - - if (normalizedRule.includes('/')) { - pathRules.push(entry); - const lowerBaseName = getMaterialBaseName(normalizedRule).toLowerCase(); - const rulesForBaseName = pathRulesByLowerBaseName.get(lowerBaseName) ?? []; - rulesForBaseName.push(entry); - pathRulesByLowerBaseName.set(lowerBaseName, rulesForBaseName); - continue; - } - - baseNameRules.set(lowerRule, entry); - } - - pathRules.sort((left, right) => right.normalizedRule.length - left.normalizedRule.length); - for (const rulesForBaseName of pathRulesByLowerBaseName.values()) { - rulesForBaseName.sort((left, right) => right.normalizedRule.length - left.normalizedRule.length); - } - - return { baseNameRules, pathRules, pathRulesByLowerBaseName }; -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/paths.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/paths.ts deleted file mode 100644 index 508d4c597..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/paths.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as path from 'node:path'; - -export function normalizePathSeparators(value: string): string { - return value.replace(/\\/g, '/'); -} - -export function getMaterialBaseName(value: string): string { - return path.posix.basename(normalizePathSeparators(value)); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts index 3dce565e8..6bc1fc105 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; +import type { MaterialIconManifest } from '@codegraphy-dev/graph-visuals'; import { unknownRecordSchema } from '../../../../../shared/values'; -import type { MaterialIconManifest } from './model'; const materialStringRecordSchema = unknownRecordSchema.transform((record): Record => Object.fromEntries( diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/svg.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/svg.ts deleted file mode 100644 index 73466f5ef..000000000 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/svg.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { DEFAULT_MATERIAL_COLOR } from './model'; - -const COLOR_PATTERN = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; - -export function extractPrimaryColor(svg: string): string { - const matches = svg.match(COLOR_PATTERN) ?? []; - if (matches.length === 0) { - return DEFAULT_MATERIAL_COLOR; - } - - const counts = new Map(); - let bestColor = DEFAULT_MATERIAL_COLOR; - let bestCount = 0; - - for (const match of matches) { - const normalized = normalizeHexColor(match); - const nextCount = (counts.get(normalized) ?? 0) + 1; - counts.set(normalized, nextCount); - if (nextCount > bestCount) { - bestColor = normalized; - bestCount = nextCount; - } - } - - return bestColor; -} - -export function toWhiteSvgDataUrl(svg: string): string { - const whiteSvg = svg.replace(COLOR_PATTERN, '#FFFFFF'); - return toSvgDataUrl(whiteSvg); -} - -export function toSvgDataUrl(svg: string): string { - return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`; -} - -function normalizeHexColor(value: string): string { - if (value.length === 4) { - const [, red, green, blue] = value; - return `#${red}${red}${green}${green}${blue}${blue}`.toUpperCase(); - } - - return value.slice(0, 7).toUpperCase(); -} diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts index e709a47c1..d7b64a8f2 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts @@ -1,11 +1,11 @@ import * as vscode from 'vscode'; +import { findMaterialMatch } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../../../shared/graph/contracts'; import type { IGroup } from '../../../../../shared/settings/groups'; import { createMaterialGroup, getManualGroups, getSpecificityScore, sortMaterialGroups } from './groups'; import { collectMaterialFileGroups } from './files'; import { collectMaterialFolderGroups } from './folders'; import { loadMaterialTheme } from './manifest'; -import { findMaterialMatch } from './match'; export { createMaterialGroup, getSpecificityScore, findMaterialMatch }; diff --git a/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts b/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts index 01724f6f9..3e5afd518 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts @@ -1,4 +1,4 @@ -import { toSvgDataUrl } from '../materialTheme/svg'; +import { toSvgDataUrl } from '@codegraphy-dev/graph-visuals'; export function createMaterialSymbolIconDataUrl(pathData: string): string { return toSvgDataUrl(``); diff --git a/packages/extension/src/extension/graphView/settings/reader.ts b/packages/extension/src/extension/graphView/settings/reader.ts index ca8aeba5f..6f534aa49 100644 --- a/packages/extension/src/extension/graphView/settings/reader.ts +++ b/packages/extension/src/extension/graphView/settings/reader.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR, normalizeHexColor } from '../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { normalizeHexColor } from '../../../shared/normalizeHexColor'; import type { BidirectionalEdgeMode, DirectionMode } from '../../../shared/settings/modes'; import { DEFAULT_SHOW_MINIMAP } from '../../../shared/settings/defaults'; diff --git a/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts b/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts index b1bdae9c1..8e88985f9 100644 --- a/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts +++ b/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts @@ -1,6 +1,6 @@ import type { WebviewToExtensionMessage } from '../../../../shared/protocol/webviewToExtension'; import type { DirectionMode } from '../../../../shared/settings/modes'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { normalizeDirectionColor } from '../../settings/reader'; import type { GraphViewSettingsMessageHandlers, diff --git a/packages/extension/src/extension/repoSettings/defaults.ts b/packages/extension/src/extension/repoSettings/defaults.ts index 5af4f2031..816bfa8f5 100644 --- a/packages/extension/src/extension/repoSettings/defaults.ts +++ b/packages/extension/src/extension/repoSettings/defaults.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGroup } from '../../shared/settings/groups'; import type { NodeSizeMode } from '../../shared/settings/modes'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; diff --git a/packages/extension/src/shared/fileColorConstants.ts b/packages/extension/src/shared/fileColorConstants.ts deleted file mode 100644 index 67756dc23..000000000 --- a/packages/extension/src/shared/fileColorConstants.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Static default color palette for built-in file extensions. - * - * These values back the current extension and webview defaults. - */ -export const FILE_TYPE_COLORS: Readonly> = Object.freeze({ - '.ts': '#93C5FD', // Soft blue - '.tsx': '#67E8F9', // Soft cyan - '.js': '#FDE68A', // Soft yellow - '.jsx': '#FDBA74', // Soft peach - '.css': '#F9A8D4', // Soft pink - '.scss': '#E879F9', // Soft magenta - '.json': '#86EFAC', // Soft green - '.md': '#CBD5E1', // Soft gray - '.html': '#FCA5A5', // Soft coral - '.svg': '#C4B5FD', // Soft purple -}) satisfies Record; - -/** - * Default color for file types not in {@link FILE_TYPE_COLORS}. - * A neutral soft zinc gray that works on dark backgrounds. - */ -export const DEFAULT_NODE_COLOR = '#A1A1AA'; // Soft zinc - -/** Default color for structural folder nodes. Same as DEFAULT_NODE_COLOR. */ -export const DEFAULT_FOLDER_NODE_COLOR = '#A1A1AA'; - -/** Default color for synthetic external package nodes. */ -export const DEFAULT_PACKAGE_NODE_COLOR = '#F59E0B'; - -/** Default color for direction indicators (arrows/particles). */ -export const DEFAULT_DIRECTION_COLOR = '#475569'; diff --git a/packages/extension/src/shared/fileColors.ts b/packages/extension/src/shared/fileColors.ts deleted file mode 100644 index e6419d5e0..000000000 --- a/packages/extension/src/shared/fileColors.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - DEFAULT_NODE_COLOR, - FILE_TYPE_COLORS, -} from './fileColorConstants'; - -export { - DEFAULT_DIRECTION_COLOR, - DEFAULT_FOLDER_NODE_COLOR, - DEFAULT_NODE_COLOR, - DEFAULT_PACKAGE_NODE_COLOR, - FILE_TYPE_COLORS, -} from './fileColorConstants'; - -/** Validates and normalizes a hex color string, returning the default if invalid. */ -export function normalizeHexColor(value: string | undefined, defaultColor: string): string { - if (!value) return defaultColor; - const trimmed = value.trim(); - if (/^#[0-9A-Fa-f]{6}$/.test(trimmed)) return trimmed.toUpperCase(); - return defaultColor; -} - -/** - * Get the display color for a file based on its extension. - * - * @param extension - File extension including leading dot (e.g., '.ts') - * @returns Hex color string for the file type - * - * @example - * ```typescript - * getFileColor('.ts') // Returns '#93C5FD' (soft blue) - * getFileColor('.tsx') // Returns '#67E8F9' (soft cyan) - * getFileColor('.xyz') // Returns '#A1A1AA' (default gray) - * ``` - */ -export function getFileColor(extension: string): string { - return FILE_TYPE_COLORS[extension.toLowerCase()] ?? DEFAULT_NODE_COLOR; -} diff --git a/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts b/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts index 7be4eb139..66eab3086 100644 --- a/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts +++ b/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../fileColors'; +} from '@codegraphy-dev/graph-visuals'; export function createStructuralGraphNodeTypes(): IGraphNodeTypeDefinition[] { return [ diff --git a/packages/extension/src/shared/mock/transforms.ts b/packages/extension/src/shared/mock/transforms.ts index 664ae04bf..7ccb6592e 100644 --- a/packages/extension/src/shared/mock/transforms.ts +++ b/packages/extension/src/shared/mock/transforms.ts @@ -3,7 +3,7 @@ * @module shared/mockDataTransforms */ -import { getFileColor } from '../fileColors'; +import { getFileColor } from '@codegraphy-dev/graph-visuals'; import type { IFileData } from './fileData'; import type { IGraphEdge, IGraphNode } from '../graph/contracts'; diff --git a/packages/extension/src/shared/normalizeHexColor.ts b/packages/extension/src/shared/normalizeHexColor.ts new file mode 100644 index 000000000..69711cb3f --- /dev/null +++ b/packages/extension/src/shared/normalizeHexColor.ts @@ -0,0 +1,6 @@ +/** Validates and normalizes a hex color string, returning the default if invalid. */ +export function normalizeHexColor(value: string | undefined, defaultColor: string): string { + if (!value) return defaultColor; + const trimmed = value.trim(); + return /^#[0-9A-Fa-f]{6}$/.test(trimmed) ? trimmed.toUpperCase() : defaultColor; +} diff --git a/packages/extension/src/webview/app/shell/visibleGraphResponse.ts b/packages/extension/src/webview/app/shell/visibleGraphResponse.ts index ffc533782..b8432a2f7 100644 --- a/packages/extension/src/webview/app/shell/visibleGraphResponse.ts +++ b/packages/extension/src/webview/app/shell/visibleGraphResponse.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import type { IGraphData } from '../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import { postMessage as postWebviewMessage } from '../../vscodeApi'; export function createVisibleGraphStatePayload(graphData: IGraphData | null | undefined) { diff --git a/packages/extension/src/webview/components/graph/model/build.ts b/packages/extension/src/webview/components/graph/model/build.ts index 998e6998f..64fc3bb70 100644 --- a/packages/extension/src/webview/components/graph/model/build.ts +++ b/packages/extension/src/webview/components/graph/model/build.ts @@ -13,7 +13,6 @@ import { export { processEdges } from './edgeProcessing'; import { calculateNodeSizes } from './node/sizing'; export { - DEFAULT_NODE_SIZE, FALLBACK_MUTED_NODE_COLOR, FAVORITE_BORDER_COLOR, getDepthOpacity, diff --git a/packages/extension/src/webview/components/graph/model/node/appearance.ts b/packages/extension/src/webview/components/graph/model/node/appearance.ts index ba6afc49c..0e679a948 100644 --- a/packages/extension/src/webview/components/graph/model/node/appearance.ts +++ b/packages/extension/src/webview/components/graph/model/node/appearance.ts @@ -1,4 +1,5 @@ import type { IGraphNode } from '../../../../../shared/graph/contracts'; +import { GRAPH_NODE_BORDER_WIDTH } from '@codegraphy-dev/graph-visuals'; import type { GraphAppearance } from '../../appearance/model'; import { FALLBACK_MUTED_NODE_COLOR, FAVORITE_BORDER_COLOR } from './display'; @@ -54,5 +55,5 @@ export function graphNodeBorderColor({ export function graphNodeBorderWidth(isFocused: boolean, isFavorite: boolean): number { if (isFocused) return 4; - return isFavorite ? 3 : 2; + return isFavorite ? 3 : GRAPH_NODE_BORDER_WIDTH; } diff --git a/packages/extension/src/webview/components/graph/model/node/display.ts b/packages/extension/src/webview/components/graph/model/node/display.ts index cb2a6457d..cd1bcd43f 100644 --- a/packages/extension/src/webview/components/graph/model/node/display.ts +++ b/packages/extension/src/webview/components/graph/model/node/display.ts @@ -1,8 +1,9 @@ -import { DEFAULT_DIRECTION_COLOR } from '../../../../../shared/fileColors'; +import { + DEFAULT_DIRECTION_COLOR, +} from '@codegraphy-dev/graph-visuals'; export const FAVORITE_BORDER_COLOR = '#EAB308'; export const FALLBACK_MUTED_NODE_COLOR = '#6b7280'; -export const DEFAULT_NODE_SIZE = 16; export function resolveDirectionColor(directionColor: string): string { const color = directionColor.trim(); diff --git a/packages/extension/src/webview/components/graph/model/node/sizing.ts b/packages/extension/src/webview/components/graph/model/node/sizing.ts index 510d9228e..5f53908ac 100644 --- a/packages/extension/src/webview/components/graph/model/node/sizing.ts +++ b/packages/extension/src/webview/components/graph/model/node/sizing.ts @@ -1,6 +1,6 @@ import type { IGraphEdge, IGraphNode } from '../../../../../shared/graph/contracts'; import type { NodeSizeMode } from '../../../../../shared/settings/modes'; -import { computeConnectionSizes } from '../sizing/calculations'; +import { computeConnectionSizes } from '@codegraphy-dev/graph-visuals'; import { computeFileSizeSizes } from '../sizing/fileSize'; export function calculateNodeSizes( diff --git a/packages/extension/src/webview/components/graph/model/node/styleBuild.ts b/packages/extension/src/webview/components/graph/model/node/styleBuild.ts index 09d699d58..192faf8c8 100644 --- a/packages/extension/src/webview/components/graph/model/node/styleBuild.ts +++ b/packages/extension/src/webview/components/graph/model/node/styleBuild.ts @@ -1,10 +1,14 @@ import type { IGraphNode } from '../../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '../../../../../shared/fileColors'; +import { + DEFAULT_NODE_COLOR, + DEFAULT_NODE_SIZE, + MAX_NODE_SIZE, + MIN_NODE_SIZE, +} from '@codegraphy-dev/graph-visuals'; import { adjustColorForLightTheme } from '../../../../theme/useTheme'; import type { GraphAppearance } from '../../appearance/model'; -import { MAX_NODE_SIZE, MIN_NODE_SIZE } from '../sizing/calculations'; import { graphNodeBorderColor, graphNodeBorderWidth, graphNodeDisplayColor } from './appearance'; -import { DEFAULT_NODE_SIZE, getDepthOpacity, getDepthSizeMultiplier } from './display'; +import { getDepthOpacity, getDepthSizeMultiplier } from './display'; export interface GraphNodeStyle { baseOpacity: number; borderColor: string; borderWidth: number; color: string; isFavorite: boolean; size: number } diff --git a/packages/extension/src/webview/components/graph/model/sizing/calculations.ts b/packages/extension/src/webview/components/graph/model/sizing/calculations.ts deleted file mode 100644 index ea1cf611e..000000000 --- a/packages/extension/src/webview/components/graph/model/sizing/calculations.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IGraphEdge, IGraphNode } from '../../../../../shared/graph/contracts'; - -export const MIN_NODE_SIZE = 8; -export const MAX_NODE_SIZE = 30; - -const OBSIDIAN_CONNECTION_SCALE = 3; - -function obsidianConnectionSize(relatedNodeCount: number): number { - return Math.max( - MIN_NODE_SIZE, - Math.min( - OBSIDIAN_CONNECTION_SCALE * Math.sqrt(relatedNodeCount + 1), - MAX_NODE_SIZE, - ), - ); -} - -/** Returns Obsidian-style sizes from each node's unique related nodes. */ -export function computeConnectionSizes( - nodes: IGraphNode[], - edges: Pick[] -): Map { - const relatedNodeIds = new Map(nodes.map(node => [node.id, new Set()])); - for (const edge of edges) { - const sourceRelations = relatedNodeIds.get(edge.from); - const targetRelations = relatedNodeIds.get(edge.to); - if (!sourceRelations || !targetRelations) continue; - sourceRelations.add(edge.to); - targetRelations.add(edge.from); - } - - return new Map(nodes.map(node => [ - node.id, - obsidianConnectionSize(relatedNodeIds.get(node.id)?.size ?? 0), - ])); -} diff --git a/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts b/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts index a05c8bb00..c92a45a9c 100644 --- a/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts +++ b/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts @@ -1,5 +1,5 @@ import type { IGraphNode } from '../../../../../shared/graph/contracts'; -import { MIN_NODE_SIZE, MAX_NODE_SIZE } from './calculations'; +import { MAX_NODE_SIZE, MIN_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; export function computeFileSizeSizes(nodes: IGraphNode[]): Map { const sizes = new Map(); diff --git a/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts b/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts index 1b9b1b445..93e593e73 100644 --- a/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts +++ b/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '../../../../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { resolveDirectionColor, type FGLink, diff --git a/packages/extension/src/webview/components/graph/rendering/link/metrics.ts b/packages/extension/src/webview/components/graph/rendering/link/metrics.ts index 28097947b..a0bbf9d8a 100644 --- a/packages/extension/src/webview/components/graph/rendering/link/metrics.ts +++ b/packages/extension/src/webview/components/graph/rendering/link/metrics.ts @@ -1,17 +1,19 @@ import type { FGLink } from '../../model/build'; +import { + CONNECTED_LINK_OPACITY, + LINK_BASE_WIDTH, + MUTED_LINK_OPACITY, + ORDINARY_LINK_OPACITY, +} from '@codegraphy-dev/graph-visuals'; import { resolveLinkEndpointId } from '../../support/linkTargets'; import type { LinkRenderingDependencies } from './contracts'; -const ORDINARY_LINK_OPACITY = 0.3; -const CONNECTED_LINK_OPACITY = 0.9; -const MUTED_LINK_OPACITY = 0.12; - export function getBaseGraphLinkOpacity(): number { return ORDINARY_LINK_OPACITY; } export function getBaseGraphLinkWidth(): number { - return 1; + return LINK_BASE_WIDTH; } function linkConnectsNode(link: FGLink, nodeId: string): boolean { @@ -47,6 +49,6 @@ export function getGraphLinkWidth( const decoration = dependencies.edgeDecorationsRef.current?.[link.id]; if (decoration?.width !== undefined) return decoration.width; const highlighted = dependencies.highlightedNodeRef.current; - if (!highlighted) return 1; - return linkConnectsNode(link, highlighted) ? 2 : 1; + if (!highlighted) return LINK_BASE_WIDTH; + return linkConnectsNode(link, highlighted) ? 2 : LINK_BASE_WIDTH; } diff --git a/packages/extension/src/webview/components/graph/rendering/node/label.ts b/packages/extension/src/webview/components/graph/rendering/node/label.ts index 4bb9c2be9..303ea5c15 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/label.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/label.ts @@ -1,6 +1,7 @@ import type { NodeDecorationPayload } from '../../../../../shared/plugins/decorations'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../appearance/model'; import { graphDetailOpacity } from '@codegraphy-dev/graph-renderer'; +import { graphNodeLabelTop } from '@codegraphy-dev/graph-visuals'; import type { FGNode } from '../../model/build'; import type { NodeLabelSpriteProvider } from './labelSprite'; @@ -50,7 +51,7 @@ export function renderNodeLabel({ ctx.drawImage( sprite.image, node.x! - width / 2, - node.y! + nodeHalfHeight + 2 / globalScale, + graphNodeLabelTop(node.y!, nodeHalfHeight, globalScale), width, height, ); diff --git a/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts b/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts index 2199fefe3..b43d2e76c 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts @@ -1,26 +1,31 @@ import type { NodeLabelSprite } from './labelSprite'; - -const FONT = '12px Sans-Serif'; -const PADDING = 2; +import { + GRAPH_NODE_LABEL_FONT, + GRAPH_NODE_LABEL_PADDING, +} from '@codegraphy-dev/graph-visuals'; export function rasterizeNodeLabelSprite(createCanvas: () => HTMLCanvasElement, text: string, color: string, scale: number): { backingBytes: number; sprite: NodeLabelSprite } { const canvas = createCanvas(); const measurement = canvas.getContext('2d'); if (!measurement) throw new Error('Canvas 2D is unavailable for graph labels'); - measurement.font = FONT; + measurement.font = GRAPH_NODE_LABEL_FONT; const metrics = measurement.measureText(text); const left = Number.isFinite(metrics.actualBoundingBoxLeft) ? metrics.actualBoundingBoxLeft : 0; const right = Number.isFinite(metrics.actualBoundingBoxRight) ? metrics.actualBoundingBoxRight : metrics.width; const ascent = Number.isFinite(metrics.actualBoundingBoxAscent) ? metrics.actualBoundingBoxAscent : 11; const descent = Number.isFinite(metrics.actualBoundingBoxDescent) ? metrics.actualBoundingBoxDescent : 2; - const cssWidth = Math.ceil(Math.max(metrics.width, left + right)) + PADDING; - const cssHeight = Math.ceil(ascent + descent) + PADDING; + const cssWidth = Math.ceil(Math.max(metrics.width, left + right)) + GRAPH_NODE_LABEL_PADDING; + const cssHeight = Math.ceil(ascent + descent) + GRAPH_NODE_LABEL_PADDING; canvas.width = Math.max(1, Math.ceil(cssWidth * scale)); canvas.height = Math.max(1, Math.ceil(cssHeight * scale)); const context = canvas.getContext('2d'); if (!context) throw new Error('Canvas 2D is unavailable for graph labels'); - context.scale(scale, scale); context.fillStyle = color; context.font = FONT; + context.scale(scale, scale); context.fillStyle = color; context.font = GRAPH_NODE_LABEL_FONT; context.textAlign = 'left'; context.textBaseline = 'alphabetic'; - context.fillText(text, PADDING / 2 + left, PADDING / 2 + ascent); + context.fillText( + text, + GRAPH_NODE_LABEL_PADDING / 2 + left, + GRAPH_NODE_LABEL_PADDING / 2 + ascent, + ); return { backingBytes: canvas.width * canvas.height * 4, sprite: { cssHeight, cssWidth, image: canvas } }; } diff --git a/packages/extension/src/webview/components/graph/rendering/node/media.ts b/packages/extension/src/webview/components/graph/rendering/node/media.ts index 267eddc1e..fa4864727 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/media.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/media.ts @@ -1,4 +1,5 @@ import type { NodeDecorationPayload } from '../../../../../shared/plugins/decorations'; +import { fileIconSize, folderIconSize } from '@codegraphy-dev/graph-visuals'; import { getImage } from '../imageCache'; import { drawShape } from '../shapes/draw/twoDimensional'; import type { WebviewPluginHost } from '../../../../pluginHost/manager'; @@ -26,7 +27,9 @@ export function renderNodeImageOverlay( drawShape(ctx, node.shape2D ?? 'circle', node.x!, node.y!, node.size * 0.8); ctx.clip(); } - const imageSize = node.nodeType === 'folder' ? node.size * 2 : node.size * 1.2; + const imageSize = node.nodeType === 'folder' + ? folderIconSize(node.size) + : fileIconSize(node.size); ctx.drawImage(image, node.x! - imageSize / 2, node.y! - imageSize / 2, imageSize, imageSize); ctx.restore(); } diff --git a/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts b/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts index 5b516ddcc..6f05b5951 100644 --- a/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts +++ b/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts @@ -1,7 +1,8 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, -} from '../../../../../shared/fileColors'; + GRAPH_NODE_SELECTION_BORDER_WIDTH, +} from '@codegraphy-dev/graph-visuals'; import type { FGNode } from '../../model/build'; import type { NodeCanvasRendererDependencies } from '../node/canvasShared'; import type { OwnedGraphNodeStyle } from '../surface/owned2d/view/surface/contracts'; @@ -79,7 +80,9 @@ export function getNodeCanvasStyle(dependencies: NodeCanvasRendererDependencies, const selected = dependencies.selectedNodesSetRef.current.has(node.id); return { borderColor: borderColor(dependencies, node, selected), - borderWidth: selected ? Math.max(node.borderWidth, 3) : node.borderWidth, + borderWidth: selected + ? Math.max(node.borderWidth, GRAPH_NODE_SELECTION_BORDER_WIDTH) + : node.borderWidth, cornerRadius: Math.max(0, node.cornerRadius2D ?? 0), fillColor: fillColor(dependencies, node), fillOpacity: normalizedNodeFillOpacity(node.fillOpacity2D), diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts index 4e4e0e345..885cfcfc8 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts @@ -1,6 +1,5 @@ import type { FGNode } from '../../../../../model/build'; - -export const OWNED_GRAPH_COLLISION_RADIUS_PADDING = 4; +import { OWNED_GRAPH_COLLISION_RADIUS_PADDING } from '@codegraphy-dev/graph-visuals'; type CollisionNode = Pick; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts index 4ffbdc76f..90f106f75 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts @@ -1,5 +1,6 @@ import { graphNodeSizeChargeMultiplier, GraphNodeFlag, type GraphLayoutInput } from '@codegraphy-dev/graph-renderer'; -import { DEFAULT_NODE_SIZE, type FGLink, type FGNode } from '../../../../../model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import type { FGLink, FGNode } from '../../../../../model/build'; import { ownedNodeCollisionRadius } from '../collision/radius'; export type OwnedGraphLayoutInput = GraphLayoutInput & { chargeStrengthMultipliers: Float32Array; flags: Uint8Array }; diff --git a/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts b/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts index c8cd0ecd5..4bad3af59 100644 --- a/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts +++ b/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts @@ -3,11 +3,9 @@ import type { MutableRefObject } from 'react'; import type { GraphTooltipRect, } from '../../tooltip/model'; -import { - DEFAULT_NODE_SIZE, - type FGNode, -} from '../../model/build'; +import type { FGNode } from '../../model/build'; import { graphNodeScreenRadius } from '@codegraphy-dev/graph-renderer'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; interface GraphTooltipRectOptions { containerRef: MutableRefObject; diff --git a/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts b/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts index b36a5fa03..f908fc1a7 100644 --- a/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts +++ b/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts @@ -3,7 +3,7 @@ import { type MutableRefObject, } from 'react'; import type { IGraphData } from '../../../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '../../../../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import { ThemeKind, adjustColorForLightTheme } from '../../../../../theme/useTheme'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../../appearance/model'; import type { FGLink, FGNode } from '../../../model/build'; diff --git a/packages/extension/src/webview/export/json/build/nodes.ts b/packages/extension/src/webview/export/json/build/nodes.ts index 68e5f7e1e..6ecf89070 100644 --- a/packages/extension/src/webview/export/json/build/nodes.ts +++ b/packages/extension/src/webview/export/json/build/nodes.ts @@ -1,5 +1,5 @@ import type { IGraphData } from '../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '../../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGroup } from '../../../../shared/settings/groups'; import { globMatch } from '../../../globMatch'; diff --git a/packages/extension/src/webview/export/shared/context.ts b/packages/extension/src/webview/export/shared/context.ts index 38f4cbe17..fc7451e38 100644 --- a/packages/extension/src/webview/export/shared/context.ts +++ b/packages/extension/src/webview/export/shared/context.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; const EXPORT_BACKGROUND_COLOR = '#18181b'; diff --git a/packages/extension/src/webview/export/svg/link/element/build.ts b/packages/extension/src/webview/export/svg/link/element/build.ts index 6fffea7e1..917423e88 100644 --- a/packages/extension/src/webview/export/svg/link/element/build.ts +++ b/packages/extension/src/webview/export/svg/link/element/build.ts @@ -1,5 +1,5 @@ import type { SvgExportLink, SvgPosition } from '../../contracts'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; const CURVATURE_EPSILON = 0.001; diff --git a/packages/extension/src/webview/export/svg/node/image/overlay.ts b/packages/extension/src/webview/export/svg/node/image/overlay.ts index 73e4906e9..876edc300 100644 --- a/packages/extension/src/webview/export/svg/node/image/overlay.ts +++ b/packages/extension/src/webview/export/svg/node/image/overlay.ts @@ -2,6 +2,7 @@ import { getImage } from '../../../../components/graph/rendering/imageCache'; import { svgShapePath } from '../../shape/shapes'; import type { SvgExportNode, SvgPosition } from '../../contracts'; import type { NodeShape2D } from '../../../../../shared/settings/modes'; +import { fileIconSize } from '@codegraphy-dev/graph-visuals'; function buildClipShape(node: SvgExportNode, position: SvgPosition, shape: NodeShape2D): string { if (shape === 'circle') { @@ -28,7 +29,7 @@ export function appendNodeImageOverlay( } const clipId = `clip-${node.id.replace(/[^a-zA-Z0-9]/g, '_')}`; - const imageSize = node.size * 1.2; + const imageSize = fileIconSize(node.size); definitions.push(`${buildClipShape(node, position, shape)}`); const canvas = document.createElement('canvas'); diff --git a/packages/extension/src/webview/graphControls/filtering/nodes.ts b/packages/extension/src/webview/graphControls/filtering/nodes.ts index b8f70e062..388acd271 100644 --- a/packages/extension/src/webview/graphControls/filtering/nodes.ts +++ b/packages/extension/src/webview/graphControls/filtering/nodes.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../shared/fileColors'; +} from '@codegraphy-dev/graph-visuals'; import type { IGraphNodeTypeDefinition } from '../../../shared/graphControls/contracts'; import { CORE_GRAPH_NODE_TYPES } from '../../../shared/graphControls/defaults/nodeTypes'; import { symbolMatchesScopedDefinition } from '../../../shared/visibleGraph/scope/symbolMatch'; diff --git a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts index 7d6bc8877..26df7aaa6 100644 --- a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts +++ b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts @@ -1,4 +1,4 @@ -import { DEFAULT_NODE_COLOR } from '../../../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../../../shared/graph/contracts'; import { normalizeNodeLegendRules } from './compile'; import type { diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index 232fe4a88..622b81595 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -1,6 +1,6 @@ import type { GraphStateFields } from './state'; import { DEFAULT_PHYSICS, DEFAULT_SEARCH_OPTIONS } from './defaults'; -import { DEFAULT_DIRECTION_COLOR } from '../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; export const INITIAL_STATE: GraphStateFields = { diff --git a/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts b/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts index 29cc2aae8..472148c74 100644 --- a/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts +++ b/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts @@ -1,15 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mergeEdgeTypes, mergeNodeTypes } from '../../../../../src/extension/graphView/controls/send/definitions/merge'; import { STRUCTURAL_NESTS_EDGE_KIND } from '../../../../../src/shared/graphControls/defaults/definitions'; -import { normalizeHexColor } from '../../../../../src/shared/fileColors'; +import { normalizeHexColor } from '../../../../../src/shared/normalizeHexColor'; import { prettifyIdentifier } from '../../../../../src/extension/graphView/controls/send/definitions/identifiers'; -vi.mock('../../../../../src/shared/fileColors', async () => { - const actual = await vi.importActual('../../../../../src/shared/fileColors'); - return { - ...actual, - normalizeHexColor: vi.fn(), - }; +vi.mock('../../../../../src/shared/normalizeHexColor', () => { + return { normalizeHexColor: vi.fn() }; }); vi.mock('../../../../../src/extension/graphView/controls/send/definitions/identifiers', () => ({ diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts index e887ec871..cdaa517a9 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialExtensionMatcher, findLongestExtensionMatch, findLongestExtensionMatchWithMatcher, -} from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/extensionMatch'; +} from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/extensionMatch', () => { it('matches bare extension filenames', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts index e4f48e4f9..502569404 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileExtension } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/fileExtension'; +import { matchMaterialFileExtension } from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/fileExtension', () => { it('matches dotted suffixes and exact extension names', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts index e9a88bdef..900759aa2 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileName } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/fileName'; +import { matchMaterialFileName } from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/fileName', () => { it('matches basename and nested suffix rules', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts index 3da07d46f..7b90054fe 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFileGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/files'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { createMaterialPathRuleMatcher } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/pathMatch'; +import { createMaterialPathRuleMatcher } from '@codegraphy-dev/graph-visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts index de0e222c9..8b2493a29 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFolderName } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/folderName'; +import { matchMaterialFolderName } from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/folderName', () => { it('matches basename folder rules across the tree', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts index 95582b878..758c8f20f 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFolderGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/folders'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { MATERIAL_TRANSPARENT_NODE_COLOR } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/groups'; +import { MATERIAL_TRANSPARENT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts index 10913e33c..d326de539 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialLanguageFallback } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/languageFallback'; +import { matchMaterialLanguageFallback } from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/languageFallback', () => { it('uses language fallback rules when the manifest defines the language id', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts index 161dd4a6c..cf70dc901 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialPathRuleMatcher, findLongestPathMatch, findLongestPathMatchWithMatcher, -} from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/pathMatch'; +} from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/pathMatch', () => { it('matches basename rules case-insensitively', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts index 53a978ee9..5c7b53e1d 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts @@ -1,6 +1,6 @@ import { Buffer } from 'node:buffer'; import { describe, expect, it } from 'vitest'; -import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/svg'; +import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '@codegraphy-dev/graph-visuals'; describe('graphView/materialTheme/svg', () => { it('extracts the most common hex color and falls back when none exist', () => { diff --git a/packages/extension/tests/extension/graphView/settings/reader.test.ts b/packages/extension/tests/extension/graphView/settings/reader.test.ts index 1abcda7fa..9a90189ba 100644 --- a/packages/extension/tests/extension/graphView/settings/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import * as vscode from 'vscode'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import { getGraphViewConfigTarget, normalizeDirectionColor, diff --git a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts index ab40db3e2..6489fced1 100644 --- a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts +++ b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts @@ -4,7 +4,7 @@ import { buildGraphViewSettingsMessages, } from '../../../../src/extension/graphView/settings/messages'; import { captureGraphViewSettingsSnapshot } from '../../../../src/extension/graphView/settings/snapshot'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_MAX_FILES } from '../../../../src/shared/settings/defaults'; function createConfig(values: Record) { diff --git a/packages/extension/tests/extension/repoSettings/defaults.test.ts b/packages/extension/tests/extension/repoSettings/defaults.test.ts index 07faead61..497217d2b 100644 --- a/packages/extension/tests/extension/repoSettings/defaults.test.ts +++ b/packages/extension/tests/extension/repoSettings/defaults.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { CODEGRAPHY_MARKDOWN_PLUGIN_ID } from '@codegraphy-dev/core'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_MAX_FILES } from '../../../src/shared/settings/defaults'; import { createDefaultEdgeVisibility, diff --git a/packages/extension/tests/shared/fileColors.test.ts b/packages/extension/tests/shared/fileColors.test.ts index 6d1b0d108..cd8ac9e82 100644 --- a/packages/extension/tests/shared/fileColors.test.ts +++ b/packages/extension/tests/shared/fileColors.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from 'vitest'; import { - normalizeHexColor, DEFAULT_NODE_COLOR, DEFAULT_FOLDER_NODE_COLOR, DEFAULT_DIRECTION_COLOR, FILE_TYPE_COLORS, getFileColor, -} from '../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-visuals'; +import { normalizeHexColor } from '../../src/shared/normalizeHexColor'; describe('normalizeHexColor', () => { it('returns the default color when value is undefined', () => { diff --git a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts index 151aabc4f..f2ffbcf8b 100644 --- a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts +++ b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-visuals'; import { CORE_GRAPH_NODE_TYPES, createCoreGraphNodeTypes, diff --git a/packages/extension/tests/shared/mockData.test.ts b/packages/extension/tests/shared/mockData.test.ts index 529ce0acc..5f2fb7f79 100644 --- a/packages/extension/tests/shared/mockData.test.ts +++ b/packages/extension/tests/shared/mockData.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { getMockGraphData, MOCK_FILE_DATA } from '../../src/shared/mockData'; -import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '../../src/shared/fileColors'; +import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; describe('Mock Data', () => { describe('MOCK_FILE_DATA', () => { diff --git a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx index 0e97ffc4c..e45b5ddf1 100644 --- a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx index 55faba224..1ad8eec8c 100644 --- a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx index 3ca7bcb87..c9da86bdb 100644 --- a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx index 8d165d31d..5764628c0 100644 --- a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx index d1ca32fcf..fe1f4bc40 100644 --- a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx +++ b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/view/fixture.ts b/packages/extension/tests/webview/app/shell/view/fixture.ts index 33df44f0b..e5bcc3501 100644 --- a/packages/extension/tests/webview/app/shell/view/fixture.ts +++ b/packages/extension/tests/webview/app/shell/view/fixture.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { graphStore } from '../../../../../src/webview/store/state'; export const messageListeners: Array<(event: MessageEvent) => void> = []; diff --git a/packages/extension/tests/webview/export/shared/context.test.ts b/packages/extension/tests/webview/export/shared/context.test.ts index bfcb03339..af3b93238 100644 --- a/packages/extension/tests/webview/export/shared/context.test.ts +++ b/packages/extension/tests/webview/export/shared/context.test.ts @@ -4,7 +4,7 @@ import { createImageExportDataUrl, resolveDirectionColor, } from '../../../../src/webview/export/shared/context'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; afterEach(() => { vi.restoreAllMocks(); diff --git a/packages/extension/tests/webview/export/svg/link/document.test.ts b/packages/extension/tests/webview/export/svg/link/document.test.ts index 5dcc86f57..8b273e0a5 100644 --- a/packages/extension/tests/webview/export/svg/link/document.test.ts +++ b/packages/extension/tests/webview/export/svg/link/document.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { assembleSvg, createBaseParts, createDefinitions, getPalette } from '../../../../../src/webview/export/svg/link/document'; import type { SvgExportOptions } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/element/build.test.ts b/packages/extension/tests/webview/export/svg/link/element/build.test.ts index 3eb34887f..ebf145d13 100644 --- a/packages/extension/tests/webview/export/svg/link/element/build.test.ts +++ b/packages/extension/tests/webview/export/svg/link/element/build.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { buildLinkElement } from '../../../../../../src/webview/export/svg/link/element/build'; import type { SvgExportLink } from '../../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/links.test.ts b/packages/extension/tests/webview/export/svg/link/links.test.ts index c7fa0fe59..135fe8691 100644 --- a/packages/extension/tests/webview/export/svg/link/links.test.ts +++ b/packages/extension/tests/webview/export/svg/link/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { appendLinkElements } from '../../../../../src/webview/export/svg/link/links'; import type { SvgExportLink } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/graph/drag.test.tsx b/packages/extension/tests/webview/graph/drag.test.tsx index f7f7c6411..5482e3172 100644 --- a/packages/extension/tests/webview/graph/drag.test.tsx +++ b/packages/extension/tests/webview/graph/drag.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, act } from '@testing-library/react'; import Graph from '../../../src/webview/components/graph/view/component'; import { toOwnedPhysicsConfig } from '../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import { graphStore } from '../../../src/webview/store/state'; import OwnedGraphSurface from '../../__mocks__/ownedGraphSurface'; diff --git a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx index 8346794a5..651069051 100644 --- a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx +++ b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest'; import OwnedGraphSurface from '../../../__mocks__/ownedGraphSurface'; import Graph from '../../../../src/webview/components/graph/view/component'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { graphStore } from '../../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/graph/model/index.test.ts b/packages/extension/tests/webview/graph/model/index.test.ts index 2aadfe007..b62a90ddf 100644 --- a/packages/extension/tests/webview/graph/model/index.test.ts +++ b/packages/extension/tests/webview/graph/model/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { buildGraphData, diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts index dce256721..a96f5f367 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts @@ -5,7 +5,7 @@ import { getDepthSizeMultiplier, getNodeType, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; describe('nodeDisplay (mutation targets)', () => { describe('resolveDirectionColor', () => { diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts index 36b080594..aa4ee73f7 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts @@ -4,7 +4,7 @@ import { getDepthOpacity, getDepthSizeMultiplier, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; describe('nodeDisplay (mutation kill tests)', () => { /** diff --git a/packages/extension/tests/webview/graph/model/node/display.test.ts b/packages/extension/tests/webview/graph/model/node/display.test.ts index 18618f408..3580ac9a4 100644 --- a/packages/extension/tests/webview/graph/model/node/display.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; import { - DEFAULT_NODE_SIZE, FAVORITE_BORDER_COLOR, getDepthOpacity, getDepthSizeMultiplier, diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts index f9b35cf4a..31b078c6c 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { computeConnectionSizes } from '../../../../../src/webview/components/graph/model/sizing/calculations'; +import { computeConnectionSizes } from '@codegraphy-dev/graph-visuals'; function relatedGraph(relatedNodeCount: number) { const leaves = Array.from({ length: relatedNodeCount }, (_, index) => ({ diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts index f20143293..9f29d1b50 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts @@ -3,7 +3,7 @@ import { MIN_NODE_SIZE, MAX_NODE_SIZE, computeConnectionSizes, -} from '../../../../../src/webview/components/graph/model/sizing/calculations'; +} from '@codegraphy-dev/graph-visuals'; describe('semantic node size range', () => { it('matches Obsidian’s bounded radius domain', () => { diff --git a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts index 0158c0bf9..aa84ed0e9 100644 --- a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts +++ b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { EdgeDecorationPayload } from '../../../../../../src/shared/plugins/decorations'; import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/components/graph/appearance/model'; import type { FGLink } from '../../../../../../src/webview/components/graph/model/build'; diff --git a/packages/extension/tests/webview/graph/rendering/links.test.ts b/packages/extension/tests/webview/graph/rendering/links.test.ts index ebf8b28e9..ba59f2e56 100644 --- a/packages/extension/tests/webview/graph/rendering/links.test.ts +++ b/packages/extension/tests/webview/graph/rendering/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { EdgeDecorationPayload } from '../../../../src/shared/plugins/decorations'; import type { FGLink } from '../../../../src/webview/components/graph/model/build'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../../../src/webview/components/graph/appearance/model'; diff --git a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts index c8f52ebe9..5e7f57e95 100644 --- a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts +++ b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { FGNode } from '../../../../../src/webview/components/graph/model/build'; -import { DEFAULT_NODE_SIZE } from '../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; import { getTooltipNodeRect } from '../../../../../src/webview/components/graph/runtime/tooltip/rect'; describe('getTooltipNodeRect', () => { diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts index 8bf81faad..1ed39c290 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - DEFAULT_NODE_SIZE, - FAVORITE_BORDER_COLOR, -} from '../../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { FAVORITE_BORDER_COLOR } from '../../../../../../src/webview/components/graph/model/build'; import { applyNodeAppearance } from '../../../../../../src/webview/components/graph/runtime/use/indicators/nodeAppearance'; import { adjustColorForLightTheme } from '../../../../../../src/webview/theme/useTheme'; import { diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts index 5950b4e6d..85518e1a0 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts @@ -1,9 +1,9 @@ import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/components/graph/appearance/model'; import { - DEFAULT_NODE_SIZE, type FGNode, } from '../../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; export const DARK_APPEARANCE = { ...DEFAULT_GRAPH_APPEARANCE, diff --git a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts index d4f021da8..5ef0032e2 100644 --- a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts +++ b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-visuals'; function node(id: string, nodeType?: string, color = ''): IGraphNode { return { @@ -105,7 +105,7 @@ describe('webview/graphControls/filtering nodes', () => { it('uses node-type-specific fallback colors when no color is configured', async () => { vi.resetModules(); - vi.doMock('../../../../src/shared/fileColors', () => ({ + vi.doMock('@codegraphy-dev/graph-visuals', () => ({ DEFAULT_FOLDER_NODE_COLOR: '#folder', DEFAULT_NODE_COLOR: '#file', DEFAULT_PACKAGE_NODE_COLOR: '#package', @@ -126,7 +126,7 @@ describe('webview/graphControls/filtering nodes', () => { { ...node('a.ts'), color: '#file' }, ]); } finally { - vi.doUnmock('../../../../src/shared/fileColors'); + vi.doUnmock('@codegraphy-dev/graph-visuals'); vi.resetModules(); } }); diff --git a/packages/extension/tests/webview/search/filtering.test.ts b/packages/extension/tests/webview/search/filtering.test.ts index 915013cf6..eb873be03 100644 --- a/packages/extension/tests/webview/search/filtering.test.ts +++ b/packages/extension/tests/webview/search/filtering.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import type { IGroup } from '../../../src/shared/settings/groups'; import { diff --git a/packages/extension/tests/webview/search/filtering/rules.test.ts b/packages/extension/tests/webview/search/filtering/rules.test.ts index 4aa8ebc02..9f718d13e 100644 --- a/packages/extension/tests/webview/search/filtering/rules.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import type { IGroup } from '../../../../src/shared/settings/groups'; import { applyLegendRules } from '../../../../src/webview/search/filtering/rules'; diff --git a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts index 7a068c8eb..8bab2193e 100644 --- a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; import { applyNodeLegendRules, compileNodeLegendRules, diff --git a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx index 84f573652..05e09b2a2 100644 --- a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { IPhysicsSettings } from '../../../src/shared/settings/physics'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import SettingsPanel from '../../../src/webview/components/settingsPanel/Drawer'; import { graphStore } from '../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/store/state/defaults.test.ts b/packages/extension/tests/webview/store/state/defaults.test.ts index a90c8cafc..7914287f1 100644 --- a/packages/extension/tests/webview/store/state/defaults.test.ts +++ b/packages/extension/tests/webview/store/state/defaults.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_MAX_FILES } from '../../../../src/shared/settings/defaults'; import { createGraphStore } from '../../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts b/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts index d288a7961..c19bce822 100644 --- a/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts +++ b/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { createGraphStore } from '../../../../../src/webview/store/state'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import { clearSentMessages } from '../../../../helpers/sentMessages'; describe('GraphStore graph messages',()=>{ diff --git a/packages/graph-visuals/package.json b/packages/graph-visuals/package.json new file mode 100644 index 000000000..e545c24d0 --- /dev/null +++ b/packages/graph-visuals/package.json @@ -0,0 +1,25 @@ +{ + "name": "@codegraphy-dev/graph-visuals", + "version": "0.1.0", + "private": true, + "description": "Shared visual semantics for CodeGraphy graph interfaces", + "license": "MIT", + "type": "module", + "sideEffects": false, + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vitest": "^3.2.4" + } +} diff --git a/packages/graph-visuals/src/graph.ts b/packages/graph-visuals/src/graph.ts new file mode 100644 index 000000000..16637f150 --- /dev/null +++ b/packages/graph-visuals/src/graph.ts @@ -0,0 +1,92 @@ +export const FILE_TYPE_COLORS: Readonly> = Object.freeze({ + '.ts': '#93C5FD', + '.tsx': '#67E8F9', + '.js': '#FDE68A', + '.jsx': '#FDBA74', + '.css': '#F9A8D4', + '.scss': '#E879F9', + '.json': '#86EFAC', + '.md': '#CBD5E1', + '.html': '#FCA5A5', + '.svg': '#C4B5FD', +}); + +export const DEFAULT_NODE_COLOR = '#A1A1AA'; +export const DEFAULT_FOLDER_NODE_COLOR = '#A1A1AA'; +export const DEFAULT_PACKAGE_NODE_COLOR = '#F59E0B'; +export const DEFAULT_DIRECTION_COLOR = '#475569'; + +export const MIN_NODE_SIZE = 8; +export const MAX_NODE_SIZE = 30; +export const DEFAULT_NODE_SIZE = 16; + +export const GRAPH_NODE_BORDER_WIDTH = 2; +export const GRAPH_NODE_SELECTION_BORDER_WIDTH = 3; +export const GRAPH_NODE_LABEL_FONT = '12px Sans-Serif'; +export const GRAPH_NODE_LABEL_PADDING = 2; +export const OWNED_GRAPH_COLLISION_RADIUS_PADDING = 4; + +export const FILE_ICON_SCALE = 1.2; +export const FOLDER_ICON_SCALE = 2; + +export const ORDINARY_LINK_OPACITY = 0.3; +export const CONNECTED_LINK_OPACITY = 0.9; +export const MUTED_LINK_OPACITY = 0.12; +export const LINK_BASE_WIDTH = 1; + +export const MATERIAL_TRANSPARENT_NODE_COLOR = 'rgba(0, 0, 0, 0)'; + +interface GraphVisualNode { + id: string; +} + +interface GraphVisualEdge { + from: string; + to: string; +} + +export function getFileColor(extension: string): string { + return FILE_TYPE_COLORS[extension.toLowerCase()] ?? DEFAULT_NODE_COLOR; +} + +export function fileIconSize(nodeSize: number): number { + return nodeSize * FILE_ICON_SCALE; +} + +export function folderIconSize(nodeSize: number): number { + return nodeSize * FOLDER_ICON_SCALE; +} + +export function graphNodeLabelTop( + nodeY: number, + nodeHalfHeight: number, + globalScale: number, +): number { + return nodeY + nodeHalfHeight + GRAPH_NODE_LABEL_PADDING / globalScale; +} + +export function computeConnectionSizes< + Node extends GraphVisualNode, + Edge extends GraphVisualEdge, +>( + nodes: readonly Node[], + edges: readonly Edge[], +): Map { + const relatedNodeIds = new Map(nodes.map(node => [node.id, new Set()])); + for (const edge of edges) { + const sourceRelations = relatedNodeIds.get(edge.from); + const targetRelations = relatedNodeIds.get(edge.to); + if (!sourceRelations || !targetRelations) continue; + sourceRelations.add(edge.to); + targetRelations.add(edge.from); + } + + return new Map(nodes.map(node => [ + node.id, + connectionSize(relatedNodeIds.get(node.id)?.size ?? 0), + ])); +} + +function connectionSize(connectionCount: number): number { + return Math.max(MIN_NODE_SIZE, Math.min(3 * Math.sqrt(connectionCount + 1), MAX_NODE_SIZE)); +} diff --git a/packages/graph-visuals/src/index.ts b/packages/graph-visuals/src/index.ts new file mode 100644 index 000000000..3f9c36eb1 --- /dev/null +++ b/packages/graph-visuals/src/index.ts @@ -0,0 +1,2 @@ +export * from './graph'; +export * from './materialTheme'; diff --git a/packages/graph-visuals/src/materialTheme.ts b/packages/graph-visuals/src/materialTheme.ts new file mode 100644 index 000000000..fe86319c5 --- /dev/null +++ b/packages/graph-visuals/src/materialTheme.ts @@ -0,0 +1,329 @@ +export interface MaterialIconManifest { + fileExtensions?: Record; + fileNames?: Record; + folder?: string; + folderNames?: Record; + folderNamesExpanded?: Record; + iconDefinitions?: Record; + languageIds?: Record; + rootFolder?: string; +} + +export interface MaterialMatch { + iconName: string; + key: string; + kind: 'fileExtension' | 'fileName' | 'folderName'; +} + +export interface MaterialExtensionMatcher { + iconNameByLowerExtension: Map; +} + +export interface MaterialPathRuleEntry { + iconName: string; + lowerRule: string; + normalizedRule: string; +} + +export interface MaterialPathRuleMatcher { + baseNameRules: Map; + pathRules: MaterialPathRuleEntry[]; + pathRulesByLowerBaseName: Map; +} + +export interface MaterialThemePathMatchers { + fileNames?: MaterialPathRuleMatcher; + folderNames?: MaterialPathRuleMatcher; + folderNamesExpanded?: MaterialPathRuleMatcher; +} + +export const DEFAULT_MATERIAL_COLOR = '#90A4AE'; + +const COLOR_PATTERN = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; + +const LANGUAGE_FALLBACKS: ReadonlyArray<{ + extensions: readonly string[]; + languageId: string; +}> = [ + { languageId: 'javascript', extensions: ['js', 'cjs', 'mjs', 'esx'] }, + { languageId: 'typescript', extensions: ['ts', 'cts', 'mts', 'ets'] }, + { languageId: 'javascriptreact', extensions: ['jsx'] }, + { languageId: 'typescriptreact', extensions: ['tsx'] }, + { languageId: 'markdown', extensions: ['markdown', 'md', 'rst'] }, + { languageId: 'python', extensions: ['py', 'pyi'] }, + { languageId: 'java', extensions: ['java'] }, + { languageId: 'csharp', extensions: ['cs'] }, + { languageId: 'rust', extensions: ['rs'] }, + { languageId: 'go', extensions: ['go'] }, + { languageId: 'vue', extensions: ['vue'] }, + { languageId: 'html', extensions: ['html', 'htm', 'xhtml'] }, + { languageId: 'css', extensions: ['css'] }, + { languageId: 'sass', extensions: ['scss', 'sass'] }, + { languageId: 'less', extensions: ['less'] }, + { languageId: 'yaml', extensions: ['yaml', 'yml'] }, + { languageId: 'xml', extensions: ['xml', 'plist', 'xsd', 'dtd', 'xsl', 'xslt', 'resx'] }, + { languageId: 'toml', extensions: ['toml'] }, + { languageId: 'json', extensions: ['json', 'jsonc', 'json5', 'jsonl'] }, + { languageId: 'sql', extensions: ['sql'] }, + { languageId: 'php', extensions: ['php'] }, + { languageId: 'ruby', extensions: ['rb'] }, + { languageId: 'lua', extensions: ['lua'] }, + { languageId: 'swift', extensions: ['swift'] }, + { languageId: 'dart', extensions: ['dart'] }, + { languageId: 'svelte', extensions: ['svelte'] }, + { languageId: 'c', extensions: ['c', 'h'] }, + { languageId: 'cpp', extensions: ['cc', 'cpp', 'cxx', 'hh', 'hpp', 'hxx'] }, +]; + +export function createMaterialExtensionMatcher( + extensions: Record, +): MaterialExtensionMatcher { + return { + iconNameByLowerExtension: new Map( + Object.entries(extensions).map(([extension, iconName]) => [extension.toLowerCase(), iconName]), + ), + }; +} + +export function findLongestExtensionMatch( + baseName: string, + entries: Iterable, +): MaterialMatch | undefined { + return findLongestExtensionMatchWithMatcher( + baseName, + createMaterialExtensionMatcher(Object.fromEntries(entries)), + ); +} + +export function findLongestExtensionMatchWithMatcher( + baseName: string, + matcher: MaterialExtensionMatcher, +): MaterialMatch | undefined { + const lowerBaseName = baseName.toLowerCase(); + let bestMatch: MaterialMatch | undefined; + for (const extension of getExtensionCandidates(lowerBaseName)) { + const iconName = matcher.iconNameByLowerExtension.get(extension); + if (!iconName) continue; + const match = createExtensionMatch(baseName, lowerBaseName, extension, iconName); + if (match && (!bestMatch || bestMatch.key.length < match.key.length)) bestMatch = match; + } + return bestMatch; +} + +export function matchMaterialFileExtension( + baseName: string, + fileExtensions: Record, + matcher: MaterialExtensionMatcher = createMaterialExtensionMatcher(fileExtensions), +): MaterialMatch | undefined { + return findLongestExtensionMatchWithMatcher(baseName, matcher); +} + +export function createMaterialPathRuleMatcher( + rules: Record, +): MaterialPathRuleMatcher { + const baseNameRules = new Map(); + const pathRules: MaterialPathRuleEntry[] = []; + const pathRulesByLowerBaseName = new Map(); + for (const [ruleKey, iconName] of Object.entries(rules)) { + const normalizedRule = normalizePathSeparators(ruleKey); + const lowerRule = normalizedRule.toLowerCase(); + const entry = { iconName, lowerRule, normalizedRule }; + if (!normalizedRule.includes('/')) { + baseNameRules.set(lowerRule, entry); + continue; + } + pathRules.push(entry); + const lowerBaseName = getMaterialBaseName(normalizedRule).toLowerCase(); + const rulesForBaseName = pathRulesByLowerBaseName.get(lowerBaseName) ?? []; + rulesForBaseName.push(entry); + pathRulesByLowerBaseName.set(lowerBaseName, rulesForBaseName); + } + pathRules.sort(longestRuleFirst); + for (const rulesForBaseName of pathRulesByLowerBaseName.values()) { + rulesForBaseName.sort(longestRuleFirst); + } + return { baseNameRules, pathRules, pathRulesByLowerBaseName }; +} + +export function findLongestPathMatch( + subjectPath: string, + rules: Record, + kind: 'fileName' | 'folderName', +): MaterialMatch | undefined { + return findLongestPathMatchWithMatcher(subjectPath, createMaterialPathRuleMatcher(rules), kind); +} + +export function findLongestPathMatchWithMatcher( + subjectPath: string, + matcher: MaterialPathRuleMatcher, + kind: 'fileName' | 'folderName', +): MaterialMatch | undefined { + const subject = normalizePathSeparators(subjectPath); + const baseName = getMaterialBaseName(subject); + const lowerSubject = subject.toLowerCase(); + const lowerBaseName = baseName.toLowerCase(); + for (const rule of matcher.pathRulesByLowerBaseName.get(lowerBaseName) ?? []) { + if (lowerSubject !== rule.lowerRule && !lowerSubject.endsWith(`/${rule.lowerRule}`)) continue; + return { + iconName: rule.iconName, + key: lowerSubject === rule.lowerRule ? subject : subject.slice(-rule.normalizedRule.length), + kind, + }; + } + const rule = matcher.baseNameRules.get(lowerBaseName); + return rule ? { iconName: rule.iconName, key: baseName, kind } : undefined; +} + +export function matchMaterialFileName( + nodeId: string, + fileNames: Record, + matcher: MaterialPathRuleMatcher = createMaterialPathRuleMatcher(fileNames), +): MaterialMatch | undefined { + return findLongestPathMatchWithMatcher(nodeId, matcher, 'fileName'); +} + +export function matchMaterialFolderName( + folderPath: string, + folderNames: Record, + folderNamesExpanded: Record = {}, + matchers: Pick = {}, +): MaterialMatch | undefined { + return findLongestPathMatchWithMatcher( + folderPath, + matchers.folderNames ?? createMaterialPathRuleMatcher(folderNames), + 'folderName', + ) ?? findLongestPathMatchWithMatcher( + folderPath, + matchers.folderNamesExpanded ?? createMaterialPathRuleMatcher(folderNamesExpanded), + 'folderName', + ); +} + +export function matchMaterialLanguageFallback( + baseName: string, + languageIds: Record, +): MaterialMatch | undefined { + const entries: Array = []; + for (const { extensions, languageId } of LANGUAGE_FALLBACKS) { + const iconName = languageIds[languageId]; + if (!iconName) continue; + for (const extension of extensions) entries.push([extension, iconName]); + } + return findLongestExtensionMatch(baseName, entries); +} + +export function findMaterialMatch( + nodeId: string, + manifest: MaterialIconManifest, + options?: { + extensionMatcher?: MaterialExtensionMatcher; + nodeType?: 'file' | 'folder'; + pathMatchers?: MaterialThemePathMatchers; + }, +): MaterialMatch | undefined { + if (options?.nodeType === 'folder') { + return manifest.folderNames + ? matchMaterialFolderName( + nodeId, + manifest.folderNames, + manifest.folderNamesExpanded, + options.pathMatchers, + ) + : undefined; + } + const baseName = getMaterialBaseName(nodeId); + if (!baseName) return undefined; + return (manifest.fileNames + ? matchMaterialFileName(nodeId, manifest.fileNames, options?.pathMatchers?.fileNames) + : undefined) + ?? (manifest.fileExtensions + ? matchMaterialFileExtension(baseName, manifest.fileExtensions, options?.extensionMatcher) + : undefined) + ?? (manifest.languageIds + ? matchMaterialLanguageFallback(baseName, manifest.languageIds) + : undefined); +} + +export function normalizePathSeparators(value: string): string { + return value.replace(/\\/g, '/'); +} + +export function getMaterialBaseName(value: string): string { + const normalized = normalizePathSeparators(value); + return normalized.slice(normalized.lastIndexOf('/') + 1); +} + +export function extractPrimaryColor(svg: string): string { + const counts = new Map(); + let bestColor = DEFAULT_MATERIAL_COLOR; + let bestCount = 0; + for (const match of svg.match(COLOR_PATTERN) ?? []) { + const normalized = normalizeHexColor(match); + const count = (counts.get(normalized) ?? 0) + 1; + counts.set(normalized, count); + if (count > bestCount) { + bestColor = normalized; + bestCount = count; + } + } + return bestColor; +} + +export function toWhiteSvgDataUrl(svg: string): string { + return toSvgDataUrl(toWhiteSvg(svg)); +} + +export function toWhiteSvg(svg: string): string { + return svg.replace(COLOR_PATTERN, '#FFFFFF'); +} + +export function toSvgDataUrl(svg: string): string { + const bytes = new TextEncoder().encode(svg); + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return `data:image/svg+xml;base64,${btoa(binary)}`; +} + +function getExtensionCandidates(lowerBaseName: string): string[] { + const candidates = [lowerBaseName]; + for ( + let index = lowerBaseName.indexOf('.'); + index >= 0; + index = lowerBaseName.indexOf('.', index + 1) + ) { + const extension = lowerBaseName.slice(index + 1); + if (extension) candidates.push(extension); + } + return candidates; +} + +function createExtensionMatch( + baseName: string, + lowerBaseName: string, + extension: string, + iconName: string, +): MaterialMatch | undefined { + const lowerExtension = extension.toLowerCase(); + if (lowerBaseName !== lowerExtension && !lowerBaseName.endsWith(`.${lowerExtension}`)) { + return undefined; + } + return { + iconName, + key: lowerBaseName === lowerExtension ? baseName : baseName.slice(-extension.length), + kind: 'fileExtension', + }; +} + +function longestRuleFirst(left: MaterialPathRuleEntry, right: MaterialPathRuleEntry): number { + return right.normalizedRule.length - left.normalizedRule.length; +} + +function normalizeHexColor(value: string): string { + if (value.length === 4) { + const red = value.charAt(1); + const green = value.charAt(2); + const blue = value.charAt(3); + return `#${red}${red}${green}${green}${blue}${blue}`.toUpperCase(); + } + return value.slice(0, 7).toUpperCase(); +} diff --git a/packages/graph-visuals/tests/index.test.ts b/packages/graph-visuals/tests/index.test.ts new file mode 100644 index 000000000..eb64ca468 --- /dev/null +++ b/packages/graph-visuals/tests/index.test.ts @@ -0,0 +1,123 @@ +import { Buffer } from 'node:buffer'; +import { describe, expect, it } from 'vitest'; +import { + CONNECTED_LINK_OPACITY, + DEFAULT_DIRECTION_COLOR, + DEFAULT_FOLDER_NODE_COLOR, + DEFAULT_NODE_COLOR, + DEFAULT_NODE_SIZE, + DEFAULT_PACKAGE_NODE_COLOR, + FILE_TYPE_COLORS, + FILE_ICON_SCALE, + FOLDER_ICON_SCALE, + GRAPH_NODE_BORDER_WIDTH, + GRAPH_NODE_LABEL_FONT, + GRAPH_NODE_LABEL_PADDING, + GRAPH_NODE_SELECTION_BORDER_WIDTH, + LINK_BASE_WIDTH, + MATERIAL_TRANSPARENT_NODE_COLOR, + MAX_NODE_SIZE, + MIN_NODE_SIZE, + MUTED_LINK_OPACITY, + OWNED_GRAPH_COLLISION_RADIUS_PADDING, + ORDINARY_LINK_OPACITY, + computeConnectionSizes, + extractPrimaryColor, + fileIconSize, + findMaterialMatch, + folderIconSize, + graphNodeLabelTop, + getFileColor, + toSvgDataUrl, + toWhiteSvgDataUrl, +} from '../src/index'; + +describe('graph visual semantics', () => { + it('exports the shared palette and rendering metrics', () => { + expect(FILE_TYPE_COLORS['.ts']).toBe('#93C5FD'); + expect(getFileColor('.TS')).toBe('#93C5FD'); + expect(getFileColor('.unknown')).toBe(DEFAULT_NODE_COLOR); + expect({ + DEFAULT_DIRECTION_COLOR, + DEFAULT_FOLDER_NODE_COLOR, + DEFAULT_NODE_SIZE, + DEFAULT_PACKAGE_NODE_COLOR, + FILE_ICON_SCALE, + FOLDER_ICON_SCALE, + GRAPH_NODE_BORDER_WIDTH, + GRAPH_NODE_LABEL_FONT, + GRAPH_NODE_LABEL_PADDING, + GRAPH_NODE_SELECTION_BORDER_WIDTH, + LINK_BASE_WIDTH, + MATERIAL_TRANSPARENT_NODE_COLOR, + MAX_NODE_SIZE, + MIN_NODE_SIZE, + MUTED_LINK_OPACITY, + OWNED_GRAPH_COLLISION_RADIUS_PADDING, + ORDINARY_LINK_OPACITY, + CONNECTED_LINK_OPACITY, + }).toEqual({ + DEFAULT_DIRECTION_COLOR: '#475569', + DEFAULT_FOLDER_NODE_COLOR: '#A1A1AA', + DEFAULT_NODE_SIZE: 16, + DEFAULT_PACKAGE_NODE_COLOR: '#F59E0B', + FILE_ICON_SCALE: 1.2, + FOLDER_ICON_SCALE: 2, + GRAPH_NODE_BORDER_WIDTH: 2, + GRAPH_NODE_LABEL_FONT: '12px Sans-Serif', + GRAPH_NODE_LABEL_PADDING: 2, + GRAPH_NODE_SELECTION_BORDER_WIDTH: 3, + LINK_BASE_WIDTH: 1, + MATERIAL_TRANSPARENT_NODE_COLOR: 'rgba(0, 0, 0, 0)', + MAX_NODE_SIZE: 30, + MIN_NODE_SIZE: 8, + MUTED_LINK_OPACITY: 0.12, + OWNED_GRAPH_COLLISION_RADIUS_PADDING: 4, + ORDINARY_LINK_OPACITY: 0.3, + CONNECTED_LINK_OPACITY: 0.9, + }); + expect(fileIconSize(10)).toBe(12); + expect(folderIconSize(10)).toBe(20); + expect(graphNodeLabelTop(10, 8, 2)).toBe(19); + }); + + it('sizes nodes from distinct neighboring nodes and ignores unknown endpoints', () => { + const sizes = computeConnectionSizes( + [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + [ + { from: 'a', to: 'b' }, + { from: 'a', to: 'b' }, + { from: 'a', to: 'c' }, + { from: 'missing', to: 'a' }, + ], + ); + + expect(sizes.get('a')).toBe(8); + expect(sizes.get('b')).toBe(8); + expect(sizes.get('c')).toBe(8); + }); + + it('matches Material Theme path, extension, and language rules', () => { + expect(findMaterialMatch('apps/web/vite.config.ts', { + fileExtensions: { ts: 'typescript' }, + fileNames: { 'web/vite.config.ts': 'vite' }, + })).toEqual({ iconName: 'vite', key: 'web/vite.config.ts', kind: 'fileName' }); + expect(findMaterialMatch('README.MARKDOWN', { + languageIds: { markdown: 'markdown' }, + })).toEqual({ iconName: 'markdown', key: 'MARKDOWN', kind: 'fileExtension' }); + expect(findMaterialMatch('packages/app/src', { + folderNames: { src: 'folder-src' }, + }, { nodeType: 'folder' })).toEqual({ + iconName: 'folder-src', key: 'src', kind: 'folderName', + }); + }); + + it('extracts and transforms Material Theme SVG colors in browser-safe data URLs', () => { + const svg = ''; + expect(extractPrimaryColor(svg)).toBe('#AABBCC'); + expect(extractPrimaryColor('')).toBe('#90A4AE'); + expect(Buffer.from(toSvgDataUrl(svg).split(',')[1]!, 'base64').toString('utf8')).toBe(svg); + expect(Buffer.from(toWhiteSvgDataUrl(svg).split(',')[1]!, 'base64').toString('utf8')) + .toContain('#FFFFFF'); + }); +}); diff --git a/packages/graph-visuals/tsconfig.json b/packages/graph-visuals/tsconfig.json new file mode 100644 index 000000000..89e54cfb3 --- /dev/null +++ b/packages/graph-visuals/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "types": ["vitest/globals"] + }, + "include": ["src", "tests", "vitest.config.ts"] +} diff --git a/packages/graph-visuals/vitest.config.ts b/packages/graph-visuals/vitest.config.ts new file mode 100644 index 000000000..695b38ab7 --- /dev/null +++ b/packages/graph-visuals/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + test: { + environment: 'node', + include: ['tests/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84aafcf65..e0af6c14a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@codegraphy-dev/graph-renderer': specifier: workspace:* version: link:../../packages/graph-renderer + '@codegraphy-dev/graph-visuals': + specifier: workspace:* + version: link:../../packages/graph-visuals '@codemirror/commands': specifier: ^6.10.4 version: 6.10.4 @@ -128,6 +131,9 @@ importers: '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 + material-icon-theme: + specifier: 5.33.1 + version: 5.33.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -343,6 +349,9 @@ importers: '@codegraphy-dev/graph-renderer': specifier: workspace:* version: link:../graph-renderer + '@codegraphy-dev/graph-visuals': + specifier: workspace:* + version: link:../graph-visuals '@codegraphy-dev/plugin-api': specifier: workspace:* version: link:../plugin-api @@ -561,6 +570,15 @@ importers: specifier: ^5.3.3 version: 5.9.3 + packages/graph-visuals: + devDependencies: + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + packages/plugin-api: {} packages/plugin-godot: diff --git a/quality.config.json b/quality.config.json index 09b1f7ed5..625d4ec53 100644 --- a/quality.config.json +++ b/quality.config.json @@ -165,7 +165,6 @@ "src/**/core/plugins/versioning/apiVersions.ts", "src/**/*.d.ts", "src/**/extension/config/defaults.ts", - "src/**/shared/fileColorConstants.ts", "src/**/shared/mockData*.ts", "src/**/webview/components/icons/**", "src/**/webview/store/defaults.ts" From 489ecc0c541c9789a2012a62d59cfefde5c5dcaa Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 15:45:48 -0700 Subject: [PATCH 09/25] feat(desktop): add native workspace navigation --- apps/desktop/README.md | 2 +- apps/desktop/scripts/core-sidecar.mjs | 5 +- apps/desktop/src-tauri/src/lib.rs | 467 +++++++++++++++++- apps/desktop/src/App.tsx | 224 +++------ apps/desktop/src/bridge.ts | 55 ++- apps/desktop/src/components/CodeEditor.tsx | 19 +- apps/desktop/src/components/FileTree.tsx | 156 +++++- apps/desktop/src/components/GraphPanel.tsx | 25 +- .../src/components/WorkspaceSwitchDialog.tsx | 50 ++ .../src/components/WorkspaceSwitcher.tsx | 152 ++++++ .../src/components/createCodeEditor.ts | 70 +-- apps/desktop/src/core-sidecar.test.ts | 18 +- apps/desktop/src/model.test.ts | 29 +- apps/desktop/src/model.ts | 23 +- apps/desktop/src/useDesktopWorkspace.ts | 353 +++++++++++++ docs/RELEASING.md | 2 +- .../desktop-app-product-expectations.md | 184 +++++++ 17 files changed, 1545 insertions(+), 289 deletions(-) create mode 100644 apps/desktop/src/components/WorkspaceSwitchDialog.tsx create mode 100644 apps/desktop/src/components/WorkspaceSwitcher.tsx create mode 100644 apps/desktop/src/useDesktopWorkspace.ts create mode 100644 docs/research/desktop-app-product-expectations.md diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 4a517c21f..fc970ed6b 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -20,7 +20,7 @@ Intel is not a supported release target yet. The graph renderer has no non-WebGP - Save through an atomic replacement that preserves permissions and rejects an external edit conflict. - Read or rebuild the workspace-owned `.codegraphy/graph.sqlite` Graph Cache through Core. - Apply one-File incremental Indexing after a save. -- Show File Nodes, optional Symbol Nodes, and Relationships with the existing WebGPU and WebAssembly graph renderer. +- Show File and Folder Nodes with their Relationships through the existing WebGPU and WebAssembly graph renderer. The extension keeps its full Symbol support; the desktop Graph View is intentionally narrower for this release. Source Files and the Graph Cache stay in the workspace. The app does not upload them. diff --git a/apps/desktop/scripts/core-sidecar.mjs b/apps/desktop/scripts/core-sidecar.mjs index 0de9b8c6e..962374c9c 100644 --- a/apps/desktop/scripts/core-sidecar.mjs +++ b/apps/desktop/scripts/core-sidecar.mjs @@ -33,7 +33,6 @@ function parseRequest(value) { } return { id: value.id, - includeSymbols: value.params.includeSymbols === true, method: value.method, relativePath: value.params.relativePath, workspaceRoot: value.params.workspaceRoot, @@ -72,9 +71,7 @@ function warmWorkspaceEngine(workspaceRoot) { function graphRequest(request) { return { workspacePath: request.workspaceRoot, - ...(request.includeSymbols - ? { projection: { nodeTypes: ['file', 'folder', 'symbol'] } } - : {}), + projection: { nodeTypes: ['file', 'folder'] }, }; } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 1f37e55c6..511ae3062 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -6,18 +6,311 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::RwLock; use tauri::async_runtime::{Mutex, Receiver}; +use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; use tauri::{AppHandle, Emitter, Manager, State}; use tauri_plugin_dialog::DialogExt; use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; const MAX_EDITABLE_FILE_BYTES: u64 = 5 * 1024 * 1024; +const MAX_RECENT_WORKSPACES: usize = 8; +const RECENT_WORKSPACES_FILE_NAME: &str = "recent-workspaces.json"; + +const MENU_OPEN_WORKSPACE: &str = "file.open-workspace"; +const MENU_OPEN_RECENT_PREFIX: &str = "file.open-recent."; +const MENU_CLEAR_RECENT: &str = "file.clear-recent"; +const MENU_CLOSE_WORKSPACE: &str = "file.close-workspace"; +const MENU_SAVE: &str = "file.save"; + +const EVENT_OPEN_WORKSPACE: &str = "desktop-open-workspace"; +const EVENT_OPEN_RECENT_WORKSPACE: &str = "desktop-open-recent"; +const EVENT_RECENT_WORKSPACES_CHANGED: &str = "desktop-recent-workspaces-changed"; +const EVENT_CLOSE_WORKSPACE: &str = "desktop-close-workspace"; +const EVENT_SAVE: &str = "desktop-save"; #[derive(Default)] struct WorkspaceState { root: RwLock>, } +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct RecentWorkspaceFile { + paths: Vec, +} + +struct RecentWorkspaceStore { + file_path: PathBuf, + paths: RwLock>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +struct RecentWorkspace { + path: String, + name: String, + available: bool, +} + +impl RecentWorkspaceStore { + fn load(file_path: PathBuf) -> Result { + let paths = match fs::read(&file_path) { + Ok(bytes) => { + let stored: RecentWorkspaceFile = serde_json::from_slice(&bytes) + .map_err(|error| format!("Unable to read recent workspaces: {error}"))?; + normalize_recent_workspace_paths(stored.paths) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Vec::new(), + Err(error) => return Err(format!("Unable to read recent workspaces: {error}")), + }; + Ok(Self { + file_path, + paths: RwLock::new(paths), + }) + } + + fn list(&self) -> Result, String> { + self.paths + .read() + .map_err(|_| "Recent workspace state is unavailable.".to_string())? + .iter() + .map(|path| { + let name = path + .file_name() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .map(str::to_owned) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + Ok(RecentWorkspace { + path: path.to_string_lossy().into_owned(), + name, + available: path.is_dir(), + }) + }) + .collect() + } + + fn remember(&self, workspace_root: &Path) -> Result<(), String> { + let canonical = canonical_workspace_root(workspace_root)?; + let mut paths = self + .paths + .write() + .map_err(|_| "Recent workspace state is unavailable.".to_string())?; + paths.retain(|path| path != &canonical); + paths.insert(0, canonical); + paths.truncate(MAX_RECENT_WORKSPACES); + self.persist(&paths) + } + + fn clear(&self) -> Result<(), String> { + let mut paths = self + .paths + .write() + .map_err(|_| "Recent workspace state is unavailable.".to_string())?; + paths.clear(); + self.persist(&paths) + } + + fn persist(&self, paths: &[PathBuf]) -> Result<(), String> { + let parent = self + .file_path + .parent() + .ok_or_else(|| "Unable to resolve the recent workspace folder.".to_string())?; + fs::create_dir_all(parent) + .map_err(|error| format!("Unable to create the app configuration folder: {error}"))?; + let mut temporary = tempfile::NamedTempFile::new_in(parent) + .map_err(|error| format!("Unable to create recent workspace data: {error}"))?; + serde_json::to_writer_pretty( + temporary.as_file_mut(), + &RecentWorkspaceFile { + paths: paths.to_vec(), + }, + ) + .map_err(|error| format!("Unable to write recent workspaces: {error}"))?; + temporary + .write_all(b"\n") + .and_then(|_| temporary.flush()) + .and_then(|_| temporary.as_file().sync_all()) + .map_err(|error| format!("Unable to write recent workspaces: {error}"))?; + temporary + .persist(&self.file_path) + .map_err(|error| format!("Unable to replace recent workspaces: {}", error.error))?; + Ok(()) + } +} + +fn normalize_recent_workspace_paths(paths: Vec) -> Vec { + let mut normalized = Vec::new(); + for path in paths { + if !normalized.contains(&path) { + normalized.push(path); + } + if normalized.len() == MAX_RECENT_WORKSPACES { + break; + } + } + normalized +} + +fn build_app_menu( + app: &AppHandle, + recents: &RecentWorkspaceStore, +) -> tauri::Result> { + let codegraphy = Submenu::with_items( + app, + "CodeGraphy", + true, + &[ + &PredefinedMenuItem::about(app, None, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::services(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::hide(app, None)?, + &PredefinedMenuItem::hide_others(app, None)?, + &PredefinedMenuItem::show_all(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::quit(app, None)?, + ], + )?; + + let recent_workspaces = recents.list().map_err(std::io::Error::other)?; + let recent_items = recent_workspaces + .iter() + .enumerate() + .map(|(index, workspace)| { + MenuItem::with_id( + app, + format!("{MENU_OPEN_RECENT_PREFIX}{index}"), + &workspace.name, + workspace.available, + None::<&str>, + ) + }) + .collect::>>()?; + let empty_recent = MenuItem::with_id( + app, + "file.no-recent-workspaces", + "No Recent Workspaces", + false, + None::<&str>, + )?; + let clear_recent = MenuItem::with_id( + app, + MENU_CLEAR_RECENT, + "Clear Menu", + !recent_workspaces.is_empty(), + None::<&str>, + )?; + let open_recent = Submenu::new(app, "Open Recent", true)?; + if recent_items.is_empty() { + open_recent.append(&empty_recent)?; + } else { + for item in &recent_items { + open_recent.append(item)?; + } + } + open_recent.append(&PredefinedMenuItem::separator(app)?)?; + open_recent.append(&clear_recent)?; + + let open_workspace = MenuItem::with_id( + app, + MENU_OPEN_WORKSPACE, + "Open Workspace…", + true, + Some("CmdOrCtrl+O"), + )?; + let close_workspace = MenuItem::with_id( + app, + MENU_CLOSE_WORKSPACE, + "Close Workspace", + true, + None::<&str>, + )?; + let save = MenuItem::with_id(app, MENU_SAVE, "Save", true, Some("CmdOrCtrl+S"))?; + let file = Submenu::with_items( + app, + "File", + true, + &[ + &open_workspace, + &open_recent, + &PredefinedMenuItem::separator(app)?, + &close_workspace, + &save, + ], + )?; + let edit = Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?; + let window = Submenu::with_items( + app, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app, None)?, + &PredefinedMenuItem::maximize(app, Some("Zoom"))?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::fullscreen(app, None)?, + &PredefinedMenuItem::bring_all_to_front(app, None)?, + ], + )?; + Menu::with_items(app, &[&codegraphy, &file, &edit, &window]) +} + +fn refresh_app_menu(app: &AppHandle, recents: &RecentWorkspaceStore) -> Result<(), String> { + let menu = build_app_menu(app, recents) + .map_err(|error| format!("Unable to build the app menu: {error}"))?; + app.set_menu(menu) + .map_err(|error| format!("Unable to update the app menu: {error}"))?; + Ok(()) +} + +fn handle_menu_event(app: &AppHandle, menu_id: &str) { + let emit = |event: &str| { + let _ = app.emit(event, ()); + }; + match menu_id { + MENU_OPEN_WORKSPACE => emit(EVENT_OPEN_WORKSPACE), + MENU_CLEAR_RECENT => { + let recents = app.state::(); + if recents.clear().is_ok() && refresh_app_menu(app, &recents).is_ok() { + emit(EVENT_RECENT_WORKSPACES_CHANGED); + } + } + MENU_CLOSE_WORKSPACE => emit(EVENT_CLOSE_WORKSPACE), + MENU_SAVE => emit(EVENT_SAVE), + _ => { + let Some(index) = menu_id + .strip_prefix(MENU_OPEN_RECENT_PREFIX) + .and_then(|value| value.parse::().ok()) + else { + return; + }; + let recents = app.state::(); + let Ok(workspaces) = recents.list() else { + return; + }; + if let Some(workspace) = workspaces + .get(index) + .filter(|workspace| workspace.available) + { + let _ = app.emit(EVENT_OPEN_RECENT_WORKSPACE, workspace.path.clone()); + } + } + } +} + #[derive(Default)] struct CoreService { inner: Mutex, @@ -217,15 +510,9 @@ fn spawn_core_process(app: &AppHandle) -> Result { }) } -fn build_core_request( - request_id: u64, - action: &CoreRequestAction, - workspace_root: &Path, - include_symbols: bool, -) -> Value { +fn build_core_request(request_id: u64, action: &CoreRequestAction, workspace_root: &Path) -> Value { let mut params = json!({ "workspaceRoot": workspace_root, - "includeSymbols": include_symbols, }); if let CoreRequestAction::Update { relative_path } = action { params["relativePath"] = Value::String(relative_path.clone()); @@ -243,7 +530,6 @@ async fn request_core( service: &CoreService, action: &CoreRequestAction, workspace_root: &Path, - include_symbols: bool, ) -> Result { let mut state = service.inner.lock().await; state.next_request_id += 1; @@ -255,7 +541,7 @@ async fn request_core( .process .as_mut() .ok_or_else(|| "Core service is unavailable.".to_string())?; - let request = build_core_request(request_id, action, workspace_root, include_symbols); + let request = build_core_request(request_id, action, workspace_root); let mut request_bytes = serde_json::to_vec(&request) .map_err(|error| format!("Unable to encode Core request: {error}"))?; request_bytes.push(b'\n'); @@ -337,20 +623,13 @@ fn initial_workspace() -> Option { async fn load_workspace_graph( app: AppHandle, core: State<'_, CoreService>, + recents: State<'_, RecentWorkspaceStore>, workspace: State<'_, WorkspaceState>, workspace_root: String, reindex: bool, - include_symbols: bool, changed_path: Option, ) -> Result { let root = canonical_workspace_root(Path::new(&workspace_root))?; - { - let mut active_root = workspace - .root - .write() - .map_err(|_| "Workspace state is unavailable.".to_string())?; - *active_root = Some(root.clone()); - } let action = match (reindex, changed_path) { (true, Some(_)) => { return Err("A graph request cannot re-index and update one File.".to_string()); @@ -362,7 +641,57 @@ async fn load_workspace_graph( } (false, None) => CoreRequestAction::Open, }; - request_core(&app, &core, &action, &root, include_symbols).await + let result = request_core(&app, &core, &action, &root).await?; + { + let mut active_root = workspace + .root + .write() + .map_err(|_| "Workspace state is unavailable.".to_string())?; + *active_root = Some(root.clone()); + } + recents.remember(&root)?; + refresh_app_menu(&app, &recents)?; + app.emit(EVENT_RECENT_WORKSPACES_CHANGED, ()) + .map_err(|error| format!("Unable to report recent workspace changes: {error}"))?; + Ok(result) +} + +#[tauri::command] +fn recent_workspaces( + recents: State<'_, RecentWorkspaceStore>, +) -> Result, String> { + recents.list() +} + +#[tauri::command] +fn clear_recent_workspaces( + app: AppHandle, + recents: State<'_, RecentWorkspaceStore>, +) -> Result<(), String> { + recents.clear()?; + refresh_app_menu(&app, &recents)?; + app.emit(EVENT_RECENT_WORKSPACES_CHANGED, ()) + .map_err(|error| format!("Unable to report recent workspace changes: {error}")) +} + +#[tauri::command] +async fn close_workspace( + core: State<'_, CoreService>, + workspace: State<'_, WorkspaceState>, +) -> Result<(), String> { + { + let mut active_root = workspace + .root + .write() + .map_err(|_| "Workspace state is unavailable.".to_string())?; + *active_root = None; + } + let process = { + let mut core_state = core.inner.lock().await; + core_state.process.take() + }; + drop(process); + Ok(()) } #[tauri::command] @@ -394,15 +723,31 @@ async fn save_workspace_file( #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .enable_macos_default_menu(false) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .manage(CoreService::default()) .manage(WorkspaceState::default()) + .setup(|app| { + let recent_workspaces_path = app + .path() + .app_config_dir()? + .join(RECENT_WORKSPACES_FILE_NAME); + let recents = RecentWorkspaceStore::load(recent_workspaces_path) + .map_err(std::io::Error::other)?; + refresh_app_menu(app.handle(), &recents).map_err(std::io::Error::other)?; + app.manage(recents); + Ok(()) + }) + .on_menu_event(|app, event| handle_menu_event(app, event.id().as_ref())) .invoke_handler(tauri::generate_handler![ choose_workspace, + clear_recent_workspaces, + close_workspace, initial_workspace, load_workspace_graph, read_workspace_file, + recent_workspaces, save_workspace_file, ]) .run(tauri::generate_context!()) @@ -454,18 +799,94 @@ mod tests { } #[test] - fn update_request_names_the_saved_file() { + fn core_request_uses_the_fixed_file_and_folder_sidecar_contract() { let request = build_core_request( 7, &CoreRequestAction::Update { relative_path: "src/index.ts".to_string(), }, Path::new("/tmp/example"), - true, ); - assert_eq!(request["method"], "update"); - assert_eq!(request["params"]["relativePath"], "src/index.ts"); - assert_eq!(request["params"]["includeSymbols"], true); + assert_eq!( + request, + json!({ + "id": 7, + "kind": "request", + "method": "update", + "params": { + "relativePath": "src/index.ts", + "workspaceRoot": "/tmp/example", + }, + }) + ); + } + + #[test] + fn recent_workspaces_are_canonical_deduplicated_and_bounded() { + let config = tempfile::tempdir().expect("config folder"); + let workspaces = tempfile::tempdir().expect("workspace parent"); + let file_path = config.path().join(RECENT_WORKSPACES_FILE_NAME); + let store = RecentWorkspaceStore::load(file_path.clone()).expect("recent workspace store"); + let mut roots = Vec::new(); + for index in 0..=MAX_RECENT_WORKSPACES { + let root = workspaces.path().join(format!("workspace-{index}")); + fs::create_dir(&root).expect("create workspace"); + store.remember(&root).expect("remember workspace"); + roots.push(root.canonicalize().expect("canonical workspace")); + } + store + .remember(&roots[4]) + .expect("move existing workspace to front"); + + let recent = store.list().expect("list recent workspaces"); + assert_eq!(recent.len(), MAX_RECENT_WORKSPACES); + assert_eq!(recent[0].path, roots[4].to_string_lossy()); + assert_eq!(recent[1].path, roots[8].to_string_lossy()); + assert_eq!( + recent + .iter() + .filter(|workspace| workspace.path == roots[4].to_string_lossy()) + .count(), + 1 + ); + assert!(recent.iter().all(|workspace| workspace.available)); + + let stored: RecentWorkspaceFile = serde_json::from_slice( + &fs::read(&file_path).expect("read persisted recent workspaces"), + ) + .expect("parse persisted recent workspaces"); + assert_eq!(stored.paths[0], roots[4]); + assert_eq!(stored.paths[1], roots[8]); + assert_eq!(stored.paths.len(), MAX_RECENT_WORKSPACES); + + let reloaded = RecentWorkspaceStore::load(file_path).expect("reload recent workspaces"); + assert_eq!(reloaded.list().expect("list reloaded workspaces"), recent); + } + + #[test] + fn missing_recent_workspaces_remain_visible_and_clear_persists() { + let config = tempfile::tempdir().expect("config folder"); + let workspaces = tempfile::tempdir().expect("workspace parent"); + let file_path = config.path().join(RECENT_WORKSPACES_FILE_NAME); + let root = workspaces.path().join("moved-workspace"); + fs::create_dir(&root).expect("create workspace"); + let canonical = root.canonicalize().expect("canonical workspace"); + let store = RecentWorkspaceStore::load(file_path.clone()).expect("recent workspace store"); + store.remember(&root).expect("remember workspace"); + fs::remove_dir(&root).expect("remove workspace"); + + assert_eq!( + store.list().expect("list recent workspaces"), + vec![RecentWorkspace { + path: canonical.to_string_lossy().into_owned(), + name: "moved-workspace".to_string(), + available: false, + }] + ); + + store.clear().expect("clear recent workspaces"); + let reloaded = RecentWorkspaceStore::load(file_path).expect("reload recent workspaces"); + assert!(reloaded.list().expect("list cleared workspaces").is_empty()); } } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index b24d2eecc..b0c8d9663 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,159 +1,37 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { - chooseWorkspace, - initialWorkspace, - loadWorkspaceGraph, - readWorkspaceFile, - saveWorkspaceFile, - type FileDocument, -} from './bridge'; +import brandIconUrl from '../../../assets/icon-dark.svg?url'; import { CodeEditor } from './components/CodeEditor'; import { FileTree } from './components/FileTree'; import { GraphPanel } from './components/GraphPanel'; -import { buildFileTree, type WorkspaceGraphResult } from './model'; - -type AppStatus = - | { kind: 'idle'; message: string } - | { kind: 'busy'; message: string } - | { kind: 'ready'; message: string } - | { kind: 'error'; message: string }; - -function displayWorkspaceName(workspaceRoot: string): string { - return workspaceRoot.split('/').filter(Boolean).at(-1) ?? workspaceRoot; -} +import { WorkspaceSwitchDialog } from './components/WorkspaceSwitchDialog'; +import { WorkspaceSwitcher } from './components/WorkspaceSwitcher'; +import { useDesktopWorkspace } from './useDesktopWorkspace'; export function App(): React.ReactElement { - const [workspaceRoot, setWorkspaceRoot] = useState(); - const [graphResult, setGraphResult] = useState(); - const [status, setStatus] = useState({ - kind: 'idle', - message: 'Open a local workspace to begin.', - }); - const [includeSymbols, setIncludeSymbols] = useState(false); - const [document, setDocument] = useState(); - const [draft, setDraft] = useState(''); - const [saving, setSaving] = useState(false); - const graph = graphResult?.kind === 'ready' ? graphResult.graph : undefined; - const tree = useMemo(() => graph ? buildFileTree(graph) : [], [graph]); - const dirty = document !== undefined && draft !== document.content; - - const openGraph = useCallback(async ( - root: string, - options: { reindex: boolean; symbols: boolean; changedPath?: string }, - ): Promise => { - setStatus({ - kind: 'busy', - message: options.changedPath - ? `Core is updating ${options.changedPath}…` - : options.reindex ? 'Core is re-indexing the workspace…' : 'Loading the Graph Cache…', - }); - try { - const result = await loadWorkspaceGraph({ - workspaceRoot: root, - reindex: options.reindex, - includeSymbols: options.symbols, - changedPath: options.changedPath, - }); - setWorkspaceRoot(result.workspaceRoot); - setGraphResult(result); - if (result.kind === 'unreadable') { - setStatus({ kind: 'error', message: result.message }); - return; - } - const indexing = result.indexing - ? `${result.indexing.mode} Indexing · ${result.indexing.analyzedFiles} analyzed · ${result.indexing.reusedFiles} reused` - : result.cacheStatus.state === 'stale' ? 'Graph Cache is stale · Re-index when ready' : 'Graph Cache is current'; - setStatus({ kind: 'ready', message: indexing }); - } catch (error) { - setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); - } - }, []); - - useEffect(() => { - void initialWorkspace().then((root) => { - if (root) void openGraph(root, { reindex: false, symbols: false }); - }).catch(error => setStatus({ - kind: 'error', - message: error instanceof Error ? error.message : String(error), - })); - }, [openGraph]); - - const handleOpenWorkspace = async (): Promise => { - try { - const root = await chooseWorkspace(); - if (!root) return; - setDocument(undefined); - setDraft(''); - await openGraph(root, { reindex: false, symbols: includeSymbols }); - } catch (error) { - setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); - } - }; - - const handleSelectFile = async (path: string): Promise => { - if (document?.path === path) return; - if (dirty && !window.confirm('Discard the unsaved edit and open another File?')) return; - try { - const next = await readWorkspaceFile(path); - setDocument(next); - setDraft(next.content); - } catch (error) { - setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); - } - }; - - const handleSave = async (): Promise => { - if (!document || !dirty || saving || !workspaceRoot) return; - setSaving(true); - try { - const saved = await saveWorkspaceFile({ - relativePath: document.path, - content: draft, - expectedRevision: document.revision, - }); - setDocument(saved); - setDraft(saved.content); - await openGraph(workspaceRoot, { - reindex: false, - symbols: includeSymbols, - changedPath: saved.path, - }); - } catch (error) { - setStatus({ kind: 'error', message: error instanceof Error ? error.message : String(error) }); - } finally { - setSaving(false); - } - }; - - const handleSymbolToggle = async (): Promise => { - const next = !includeSymbols; - setIncludeSymbols(next); - if (workspaceRoot) await openGraph(workspaceRoot, { reindex: false, symbols: next }); - }; + const workspace = useDesktopWorkspace(); return (
- - CodeGraphy - {workspaceRoot ? / {displayWorkspaceName(workspaceRoot)} : null} + + + CodeGraphy + Relationship Graph +
+ void workspace.clearRecent()} + onOpenRecent={workspace.openRecentWorkspace} + onOpenWorkspace={workspace.openWorkspace} + recentWorkspaces={workspace.recentWorkspaces} + workspaceRoot={workspace.workspaceRoot} + />
- -
- {graph ? ( + {workspace.graph ? (
-
) : (
-
-

MACOS-FIRST · LOCAL BY DESIGN

-

See how your code
fits together.

-

Open any local folder. CodeGraphy will build a workspace-owned Graph Cache, then put its Files, editor, and Relationships in one focused view.

- + +

MACOS · LOCAL · FAST

+

Trace the relationships
inside your workspace.

+

Open a local folder. CodeGraphy keeps the Graph Cache beside your source and puts Files, editing, and the Relationship Graph in one window.

+
)} -
- - {status.message} +
+
+ + {workspace.pendingWorkspaceAction && workspace.document ? ( + void workspace.finishPendingWorkspaceAction(false)} + onSave={() => void workspace.finishPendingWorkspaceAction(true)} + saving={workspace.saving} + /> + ) : null}
); } diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts index a9cd219af..8f73e9992 100644 --- a/apps/desktop/src/bridge.ts +++ b/apps/desktop/src/bridge.ts @@ -1,4 +1,5 @@ import { invoke } from '@tauri-apps/api/core'; +import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { parseWorkspaceGraphResult, type WorkspaceGraphResult } from './model'; export interface FileDocument { @@ -7,6 +8,20 @@ export interface FileDocument { revision: string; } +export interface RecentWorkspace { + path: string; + name: string; + available: boolean; +} + +export interface DesktopMenuHandlers { + closeWorkspace(this: void): void; + openRecent(this: void, path: string): void; + openWorkspace(this: void): void; + recentWorkspacesChanged(this: void): void; + save(this: void): void; +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -21,6 +36,16 @@ function parseFileDocument(value: unknown): FileDocument { return { path: value.path, content: value.content, revision: value.revision }; } +function parseRecentWorkspace(value: unknown): RecentWorkspace { + if (!isRecord(value) + || typeof value.path !== 'string' + || typeof value.name !== 'string' + || typeof value.available !== 'boolean') { + throw new Error('The desktop host returned an invalid recent workspace.'); + } + return { path: value.path, name: value.name, available: value.available }; +} + export async function chooseWorkspace(): Promise { const result = await invoke('choose_workspace'); if (result === null) return undefined; @@ -38,13 +63,41 @@ export async function initialWorkspace(): Promise { export async function loadWorkspaceGraph(input: { workspaceRoot: string; reindex: boolean; - includeSymbols: boolean; changedPath?: string; }): Promise { const result = await invoke('load_workspace_graph', input); return parseWorkspaceGraphResult(result); } +export async function listRecentWorkspaces(): Promise { + const result = await invoke('recent_workspaces'); + if (!Array.isArray(result)) throw new Error('The desktop host returned invalid recent workspaces.'); + return result.map(parseRecentWorkspace); +} + +export async function clearRecentWorkspaces(): Promise { + await invoke('clear_recent_workspaces'); +} + +export async function closeWorkspace(): Promise { + await invoke('close_workspace'); +} + +export async function listenToDesktopMenu(handlers: DesktopMenuHandlers): Promise { + const unlisten = await Promise.all([ + listen('desktop-open-workspace', handlers.openWorkspace), + listen('desktop-open-recent', (event) => { + if (typeof event.payload === 'string') handlers.openRecent(event.payload); + }), + listen('desktop-recent-workspaces-changed', handlers.recentWorkspacesChanged), + listen('desktop-close-workspace', handlers.closeWorkspace), + listen('desktop-save', handlers.save), + ]); + return () => { + for (const stop of unlisten) stop(); + }; +} + export async function readWorkspaceFile(relativePath: string): Promise { return parseFileDocument(await invoke('read_workspace_file', { relativePath })); } diff --git a/apps/desktop/src/components/CodeEditor.tsx b/apps/desktop/src/components/CodeEditor.tsx index 26c4f274b..38372f7eb 100644 --- a/apps/desktop/src/components/CodeEditor.tsx +++ b/apps/desktop/src/components/CodeEditor.tsx @@ -30,15 +30,16 @@ export function CodeEditor({ let destroy: (() => void) | undefined; setLoadError(undefined); void import('./createCodeEditor') - .then(({ createCodeEditor }) => { - if (!active) return; - destroy = createCodeEditor({ - content: document.content, - onChange: content => onChangeRef.current(content), - onSave: () => onSaveRef.current(), - parent: host, - path: document.path, - }); + .then(({ createCodeEditor }) => createCodeEditor({ + content: document.content, + onChange: content => onChangeRef.current(content), + onSave: () => onSaveRef.current(), + parent: host, + path: document.path, + })) + .then((created) => { + if (active) destroy = created; + else created(); }) .catch((error: unknown) => { if (active) setLoadError(error instanceof Error ? error.message : String(error)); diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx index 4f8545063..bb3e5faa1 100644 --- a/apps/desktop/src/components/FileTree.tsx +++ b/apps/desktop/src/components/FileTree.tsx @@ -1,36 +1,103 @@ +import { useEffect, useRef, useState } from 'react'; +import { resolveMaterialIcon, type MaterialIconData } from '../materialIconTheme'; import type { FileTreeEntry } from '../model'; +function MaterialIcon({ path, mode }: { path: string; mode: 'file' | 'folder' }): React.ReactElement { + const [icon, setIcon] = useState(); + + useEffect(() => { + let active = true; + void resolveMaterialIcon(path, mode) + .then(result => { if (active) setIcon(result); }) + .catch(() => { if (active) setIcon(undefined); }); + return () => { active = false; }; + }, [mode, path]); + + return icon + ? + :
); } diff --git a/apps/desktop/src/components/GraphPanel.tsx b/apps/desktop/src/components/GraphPanel.tsx index 12acec561..7f9de80a7 100644 --- a/apps/desktop/src/components/GraphPanel.tsx +++ b/apps/desktop/src/components/GraphPanel.tsx @@ -10,19 +10,18 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import type { DesktopGraph, DesktopGraphNode } from '../model'; function nodeStyle(node: DesktopGraphNode, selectedId: string | undefined): GraphRendererNodeStyle { - const symbol = Boolean(node.symbol); const selected = node.id === selectedId; const folder = node.nodeType === 'folder'; - const size = selected ? (symbol ? 13 : 18) : (symbol ? 7 : 11); + const size = selected ? 18 : 11; return { - borderColor: selected ? '#f3fbff' : (symbol ? '#bb9cff' : '#a6e7d7'), + borderColor: selected ? '#f3fbff' : '#a6e7d7', borderWidth: selected ? 2 : 1, cornerRadius: folder ? 2 : 6, - fillColor: selected ? '#55d9b8' : (symbol ? '#7758b8' : folder ? '#c59555' : '#197c84'), + fillColor: selected ? '#55d9b8' : folder ? '#c59555' : '#197c84', fillOpacity: 1, height: folder ? size * 0.72 : size, opacity: 1, - shape: folder ? 'rectangle' : symbol ? 'diamond' : 'circle', + shape: folder ? 'rectangle' : 'circle', width: folder ? size * 1.35 : size, }; } @@ -115,8 +114,8 @@ export function GraphPanel({ } const layout = createGraphLayoutEngine({ nodeIds: nodes.map(node => node.id), - radii: Float32Array.from(graph.nodes.map(node => node.symbol ? 4 : 7)), - chargeStrengthMultipliers: Float32Array.from(graph.nodes.map(node => node.symbol ? 0.75 : 1.2)), + radii: Float32Array.from(graph.nodes, () => 7), + chargeStrengthMultipliers: Float32Array.from(graph.nodes, () => 1.2), edgeSources: Uint32Array.from(sourceIndexes), edgeTargets: Uint32Array.from(targetIndexes), }, { @@ -255,7 +254,7 @@ export function GraphPanel({ {rendererError ?
{rendererError}
: null}
Files - Symbols + Folders
@@ -271,9 +270,15 @@ export function GraphPanel({ const outgoing = edge.from === selectedId; const target = outgoing ? edge.to : edge.from; const targetNode = graph.nodes.find(node => node.id === target); - const targetFile = targetNode?.symbol?.filePath ?? target; + const targetIsFile = targetNode?.nodeType !== 'folder'; return ( - + + +
+ + ); +} diff --git a/apps/desktop/src/components/WorkspaceSwitcher.tsx b/apps/desktop/src/components/WorkspaceSwitcher.tsx new file mode 100644 index 000000000..272ee0ddc --- /dev/null +++ b/apps/desktop/src/components/WorkspaceSwitcher.tsx @@ -0,0 +1,152 @@ +import { useEffect, useRef, useState } from 'react'; +import type { RecentWorkspace } from '../bridge'; + +function workspaceName(path: string): string { + return path.split('/').filter(Boolean).at(-1) ?? path; +} + +export function WorkspaceSwitcher({ + onClearRecent, + onOpenRecent, + onOpenWorkspace, + recentWorkspaces, + workspaceRoot, +}: { + onClearRecent: () => void; + onOpenRecent: (path: string) => void; + onOpenWorkspace: () => void; + recentWorkspaces: RecentWorkspace[]; + workspaceRoot?: string; +}): React.ReactElement { + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + const buttonRef = useRef(null); + const menuRef = useRef(null); + + useEffect(() => { + if (!open) return; + const closeFromOutside = (event: PointerEvent): void => { + if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false); + }; + document.addEventListener('pointerdown', closeFromOutside); + return () => document.removeEventListener('pointerdown', closeFromOutside); + }, [open]); + + const focusMenuItem = (edge: 'first' | 'last'): void => { + const items = menuRef.current?.querySelectorAll('button:not(:disabled)'); + if (!items || items.length === 0) return; + items[edge === 'first' ? 0 : items.length - 1]?.focus(); + }; + + const handleMenuKeyDown = (event: React.KeyboardEvent): void => { + const items = [...(menuRef.current?.querySelectorAll('button:not(:disabled)') ?? [])]; + const index = document.activeElement instanceof HTMLButtonElement + ? items.indexOf(document.activeElement) + : -1; + if (event.key === 'Escape') { + event.preventDefault(); + setOpen(false); + buttonRef.current?.focus(); + return; + } + if (event.key === 'Home' || event.key === 'End') { + event.preventDefault(); + focusMenuItem(event.key === 'Home' ? 'first' : 'last'); + return; + } + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return; + event.preventDefault(); + const direction = event.key === 'ArrowDown' ? 1 : -1; + const next = index < 0 + ? direction > 0 ? 0 : items.length - 1 + : (index + direction + items.length) % items.length; + items[next]?.focus(); + }; + + const choose = (action: () => void): void => { + setOpen(false); + action(); + }; + + const activeName = workspaceRoot ? workspaceName(workspaceRoot) : 'No workspace'; + + return ( +
+ + + {open ? ( + handlePhysicsChange('linkDistance', values[0])} onValueCommit={() => flushPhysicsSetting('linkDistance')} @@ -115,9 +118,9 @@ export function ForcesSection(): React.ReactElement {
handlePhysicsChange('linkForce', values[0])} onValueCommit={() => flushPhysicsSetting('linkForce')} diff --git a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts index c0fd43828..0d5dd2903 100644 --- a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts +++ b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts @@ -1,8 +1,8 @@ -import type { IPhysicsSettings } from '../../../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; -export type PendingPhysicsMap = Partial>; +export type PendingPhysicsMap = Partial>; export type PhysicsTimerMap = Partial< - Record> + Record> >; export function clearPhysicsTimerMap(timers: PhysicsTimerMap): void { @@ -16,8 +16,8 @@ export function clearPhysicsTimerMap(timers: PhysicsTimerMap): void { export function flushPendingPhysicsValue( pendingValues: PendingPhysicsMap, timers: PhysicsTimerMap, - key: keyof IPhysicsSettings, - emit: (key: keyof IPhysicsSettings, value: number) => void, + key: keyof GraphPhysicsSettings, + emit: (key: keyof GraphPhysicsSettings, value: number) => void, ): void { const pendingValue = pendingValues[key]; if (pendingValue === undefined) { @@ -37,10 +37,10 @@ export function flushPendingPhysicsValue( export function schedulePendingPhysicsValue( pendingValues: PendingPhysicsMap, timers: PhysicsTimerMap, - key: keyof IPhysicsSettings, + key: keyof GraphPhysicsSettings, value: number, delayMs: number, - flush: (key: keyof IPhysicsSettings) => void, + flush: (key: keyof GraphPhysicsSettings) => void, ): void { pendingValues[key] = value; diff --git a/packages/extension/src/webview/store/defaults.ts b/packages/extension/src/webview/store/defaults.ts index b724dbdde..96046011a 100644 --- a/packages/extension/src/webview/store/defaults.ts +++ b/packages/extension/src/webview/store/defaults.ts @@ -1,7 +1,4 @@ import type { SearchOptions } from '../components/searchBar/field/model'; -import { DEFAULT_PHYSICS_SETTINGS, type IPhysicsSettings } from '../../shared/settings/physics'; - -export const DEFAULT_PHYSICS: IPhysicsSettings = DEFAULT_PHYSICS_SETTINGS; export const DEFAULT_SEARCH_OPTIONS: SearchOptions = { matchCase: false, diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index 622b81595..a60530acd 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -1,6 +1,9 @@ import type { GraphStateFields } from './state'; -import { DEFAULT_PHYSICS, DEFAULT_SEARCH_OPTIONS } from './defaults'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_SEARCH_OPTIONS } from './defaults'; +import { + DEFAULT_DIRECTION_COLOR, + DEFAULT_GRAPH_PHYSICS_SETTINGS, +} from '@codegraphy-dev/graph-visuals'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; export const INITIAL_STATE: GraphStateFields = { @@ -29,7 +32,7 @@ export const INITIAL_STATE: GraphStateFields = { cssSnippets: {}, graphViewportScale: null, nodeSizeMode: 'connections' as const, - physicsSettings: DEFAULT_PHYSICS, + physicsSettings: DEFAULT_GRAPH_PHYSICS_SETTINGS, depthMode: false, depthLimit: 1, maxDepthLimit: 10, diff --git a/packages/extension/src/webview/store/messageTypes.ts b/packages/extension/src/webview/store/messageTypes.ts index da8c21b6b..506ec5ddf 100644 --- a/packages/extension/src/webview/store/messageTypes.ts +++ b/packages/extension/src/webview/store/messageTypes.ts @@ -11,7 +11,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, @@ -44,7 +44,7 @@ export interface IStoreFields { cssSnippets: Record; graphViewportScale: number | null; nodeSizeMode: NodeSizeMode; - physicsSettings: IPhysicsSettings; + physicsSettings: GraphPhysicsSettings; depthMode: boolean; depthLimit: number; maxDepthLimit: number; diff --git a/packages/extension/src/webview/store/state.ts b/packages/extension/src/webview/store/state.ts index ea04729a5..896514a34 100644 --- a/packages/extension/src/webview/store/state.ts +++ b/packages/extension/src/webview/store/state.ts @@ -16,7 +16,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, @@ -48,7 +48,7 @@ export interface GraphState { cssSnippets: Record; graphViewportScale: number | null; nodeSizeMode: NodeSizeMode; - physicsSettings: IPhysicsSettings; + physicsSettings: GraphPhysicsSettings; depthMode: boolean; depthLimit: number; maxDepthLimit: number; @@ -80,7 +80,7 @@ export interface GraphState { setActivePanel: (panel: GraphState['activePanel']) => void; setGraphViewportScale: (scale: number | null) => void; setNodeSizeMode: (mode: NodeSizeMode) => void; - setPhysicsSettings: (settings: IPhysicsSettings) => void; + setPhysicsSettings: (settings: GraphPhysicsSettings) => void; setLegends: (legends: IGroup[]) => void; setOptimisticLegendUpdate: (legendId: string, updates: Partial) => void; setOptimisticLegendUpdates: (updatesByLegendId: Record>) => void; diff --git a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts index e73ec1a12..578ff186c 100644 --- a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts +++ b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import { createGraphViewProviderPhysicsSettingsMethods } from '../../../../src/extension/graphView/provider/physicsSettings'; describe('graphView/provider/physicsSettings', () => { it('reads and sends current physics settings through the provider message bridge', () => { - const readPhysicsSettings = vi.fn(() => ({ damping: 1 } as IPhysicsSettings)); + const readPhysicsSettings = vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)); const source = { _sendMessage: vi.fn() }; const configuration = { get: vi.fn((_, fallback) => fallback), @@ -16,7 +16,7 @@ describe('graphView/provider/physicsSettings', () => { readPhysicsSettings, updatePhysicsSetting: vi.fn(), resetPhysicsSettings: vi.fn(), - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }); expect(methods._getPhysicsSettings()).toEqual({ damping: 1 }); @@ -41,10 +41,10 @@ describe('graphView/provider/physicsSettings', () => { { _sendMessage: vi.fn() } as never, { getConfiguration, - readPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + readPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), updatePhysicsSetting, resetPhysicsSettings: vi.fn(async () => undefined), - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }, ); @@ -73,10 +73,10 @@ describe('graphView/provider/physicsSettings', () => { { _sendMessage: vi.fn() } as never, { getConfiguration, - readPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + readPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), updatePhysicsSetting: vi.fn(async () => undefined), resetPhysicsSettings, - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }, ); diff --git a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts index 6f14db84a..62e9b31d4 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts @@ -4,7 +4,7 @@ import path from 'path'; import * as vscode from 'vscode'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import { createGraphViewProviderSettingsStateMethods, type GraphViewProviderSettingsStateMethodDependencies, @@ -39,7 +39,7 @@ function createSource( _computeMergedGroups: vi.fn(), _sendGroupsUpdated: vi.fn(), _sendMessage: vi.fn(), - _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), ...overrides, }; diff --git a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts index 8cdadbd23..a43e76237 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { WorkspaceFolder } from 'vscode'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; const mocks = vi.hoisted(() => { let workspaceFolders: WorkspaceFolder[] | undefined = undefined; @@ -96,7 +96,7 @@ function createSource( _computeMergedGroups: vi.fn(), _sendGroupsUpdated: vi.fn(), _sendMessage: vi.fn(), - _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), ...overrides, }; diff --git a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts index 25c8fe980..ea4eca09e 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; import type * as vscode from 'vscode'; -import type { IPhysicsSettings } from '../../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import { readGraphViewPhysicsSettings } from '../../../../../src/extension/graphView/settings/physics/reader'; -const defaults: IPhysicsSettings = { +const defaults: GraphPhysicsSettings = { repelForce: 10, linkDistance: 80, linkForce: 0.15, diff --git a/packages/extension/tests/webview/graph/drag.test.tsx b/packages/extension/tests/webview/graph/drag.test.tsx index 5482e3172..1ebe8b033 100644 --- a/packages/extension/tests/webview/graph/drag.test.tsx +++ b/packages/extension/tests/webview/graph/drag.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, act } from '@testing-library/react'; import Graph from '../../../src/webview/components/graph/view/component'; -import { toOwnedPhysicsConfig } from '../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; +import { toGraphPhysicsLayoutConfig } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import { graphStore } from '../../../src/webview/store/state'; @@ -134,7 +134,7 @@ describe('Graph: owned WebGPU rendering', () => { render(); const settings = OwnedGraphSurface.getLastProps().physicsSettings; expect(settings).toBeDefined(); - expect(toOwnedPhysicsConfig(settings!).centralGravity).toBe(1); + expect(toGraphPhysicsLayoutConfig(settings!).centralGravity).toBe(1); }); it('sends PHYSICS_STABILIZED when onEngineStop fires', () => { diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts index 013ee172c..35658cb2b 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../../../../../../src/shared/settings/physics'; +import { + toGraphPhysicsLayoutConfig, + type GraphPhysicsSettings, +} from '@codegraphy-dev/graph-visuals'; import type { FGLink, FGNode } from '../../../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; import { createOwnedGraphLayout, - toOwnedPhysicsConfig, updateOwnedGraphLayout, } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; import { @@ -16,7 +18,7 @@ import { GraphNodeFlag, } from '@codegraphy-dev/graph-renderer'; -const DEFAULT_SETTINGS: IPhysicsSettings = { +const DEFAULT_SETTINGS: GraphPhysicsSettings = { centerForce: 0.1, damping: 0.4, linkDistance: 80, @@ -56,7 +58,7 @@ function run(engine: ReturnType, ticks = 240): v describe('owned graph layout settings', () => { it('maps every existing force setting to its semantic engine value', () => { - expect(toOwnedPhysicsConfig({ + expect(toGraphPhysicsLayoutConfig({ centerForce: 1, damping: 0.4, linkDistance: 500, @@ -70,7 +72,7 @@ describe('owned graph layout settings', () => { velocityDecay: 0.4, }); - expect(toOwnedPhysicsConfig({ + expect(toGraphPhysicsLayoutConfig({ centerForce: Number.POSITIVE_INFINITY, damping: -1, linkDistance: 1, @@ -87,19 +89,19 @@ describe('owned graph layout settings', () => { it('makes link distance and link force materially affect spring convergence', () => { const short = twoNodeEngine(300); - short.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); + short.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); run(short); const long = twoNodeEngine(300); - long.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 500, linkForce: 1 })); + long.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 500, linkForce: 1 })); run(long); const disabled = twoNodeEngine(300); - disabled.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 0 })); + disabled.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 0 })); run(disabled, 60); const strong = twoNodeEngine(300); - strong.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); + strong.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); run(strong, 60); const distance = (engine: ReturnType) => Math.abs(engine.x[1] - engine.x[0]); @@ -117,7 +119,7 @@ describe('owned graph layout settings', () => { edgeSources: new Uint32Array(), edgeTargets: new Uint32Array(), }); - engine.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, repelForce, centerForce })); + engine.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, repelForce, centerForce })); run(engine, 120); return engine; }; diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts index 80a382e6c..973f92b11 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_PHYSICS_SETTINGS } from '../../../../../../../../src/shared/settings/physics'; +import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-visuals'; import type { Surface2dProps } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts'; export function createDefaultSurfaceProps(): Surface2dProps { @@ -44,7 +44,7 @@ export function createDefaultSurfaceProps(): Surface2dProps { onRenderFramePost: vi.fn(), particleSize: 4, particleSpeed: 0.005, - physicsSettings: DEFAULT_PHYSICS_SETTINGS, + physicsSettings: DEFAULT_GRAPH_PHYSICS_SETTINGS, showFps: false, showMinimap: true, sharedProps: { diff --git a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts index 9ee7c7166..02a437284 100644 --- a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts +++ b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_PHYSICS_SETTINGS } from '../../../../../../src/shared/settings/physics'; +import { + DEFAULT_GRAPH_PHYSICS_SETTINGS, + applyGraphPhysicsSettings, + toGraphPhysicsLayoutConfig, +} from '@codegraphy-dev/graph-visuals'; import type { FGNode } from '../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; -import { - applyOwnedPhysicsSettings, - toOwnedPhysicsConfig, -} from '../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; import { createGraphLayoutEngine } from '@codegraphy-dev/graph-renderer'; function engine() { @@ -21,7 +21,7 @@ function engine() { describe('owned physics settings', () => { it('maps every persisted force setting into the typed engine', () => { - expect(toOwnedPhysicsConfig(DEFAULT_PHYSICS_SETTINGS)).toEqual({ + expect(toGraphPhysicsLayoutConfig(DEFAULT_GRAPH_PHYSICS_SETTINGS)).toEqual({ centralGravity: 0.1, chargeStrength: -250, linkDistance: 80, @@ -37,7 +37,7 @@ describe('owned physics settings', () => { ['linkForce', { linkForce: 0.4 }, 'linkStrength', 0.4], ['damping', { damping: 0.2 }, 'velocityDecay', 0.2], ] as const)('maps changed %s values', (_field, patch, mappedField, expected) => { - expect(toOwnedPhysicsConfig({ ...DEFAULT_PHYSICS_SETTINGS, ...patch })[mappedField]).toBe(expected); + expect(toGraphPhysicsLayoutConfig({ ...DEFAULT_GRAPH_PHYSICS_SETTINGS, ...patch })[mappedField]).toBe(expected); }); it('reheats typed physics when settings are applied', () => { @@ -45,13 +45,13 @@ describe('owned physics settings', () => { for (let tick = 0; tick < 320; tick += 1) layout.tick(); expect(layout.settled).toBe(true); - applyOwnedPhysicsSettings(layout, { ...DEFAULT_PHYSICS_SETTINGS, centerForce: 1 }); + applyGraphPhysicsSettings(layout, { ...DEFAULT_GRAPH_PHYSICS_SETTINGS, centerForce: 1 }); expect(layout.settled).toBe(false); }); it('maps persisted damping to D3 velocity decay', () => { - expect(toOwnedPhysicsConfig({ ...DEFAULT_PHYSICS_SETTINGS, damping: 0.7 }).velocityDecay) + expect(toGraphPhysicsLayoutConfig({ ...DEFAULT_GRAPH_PHYSICS_SETTINGS, damping: 0.7 }).velocityDecay) .toBe(0.7); }); diff --git a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx index 8a32bfccf..7a4aee1ce 100644 --- a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx +++ b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import type { GraphViewStoreState } from '../../../../src/webview/components/graph/view/store'; import type { UseGraphInteractionRuntimeResult } from '../../../../src/webview/components/graph/runtime/use/interaction'; import type { GraphRuntime } from '../../../../src/webview/components/graph/runtime/use/state'; @@ -208,7 +208,7 @@ export function createViewState(): Pick< GraphViewStoreState, 'bidirectionalMode' | 'depthMode' | 'directionMode' | 'favorites' | 'nodeSizeMode' | 'particleSize' | 'particleSpeed' | 'physicsSettings' | 'showFps' | 'showLabels' | 'showMinimap' > { - const physicsSettings: IPhysicsSettings = { + const physicsSettings: GraphPhysicsSettings = { centerForce: 0.1, damping: 0.42, linkDistance: 120, diff --git a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx index 05e09b2a2..b4a85fc1e 100644 --- a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; import SettingsPanel from '../../../src/webview/components/settingsPanel/Drawer'; import { graphStore } from '../../../src/webview/store/state'; @@ -11,7 +11,7 @@ vi.mock('../../../src/webview/vscodeApi', () => ({ vscode: { getState: () => undefined, setState: vi.fn() }, })); -const DEFAULT_PHYSICS: IPhysicsSettings = { +const DEFAULT_PHYSICS: GraphPhysicsSettings = { repelForce: 5, centerForce: 0.01, linkDistance: 100, diff --git a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx index 522702f64..e585c2c66 100644 --- a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx @@ -2,11 +2,12 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ForcesSection } from '../../../../src/webview/components/settingsPanel/forces/Section'; import { graphStore } from '../../../../src/webview/store/state'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({ Slider: ({ 'data-testid': testId, + min, max, step, value, @@ -14,6 +15,7 @@ vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({ onValueCommit, }: { 'data-testid'?: string; + min?: number; max?: number; step?: number; value?: number[]; @@ -26,7 +28,9 @@ vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({
-
- {workspace.document ? ( - void workspace.saveCurrentDocument()} /> - ) : ( -
- -

Choose a File

-

Browse the workspace hierarchy, make a lightweight edit, and save it back to the source File.

+ +
Files{workspace.fileCount}
+ void workspace.selectFile(path)} + selectedPath={workspace.selectedPath} + /> + + )} + editorPane={( +
+
+ {workspace.document?.path ?? 'Editor'} +
+ + +
- )} -
- - -
+ + + )} + /> ) : (
{selectedId ? 'Relationships' : 'Graph summary'} - {graph.nodes.length} Nodes · {graph.edges.length} Relationships
{selectedId ? ( <> @@ -60,14 +69,11 @@ export function GraphPanel({ {relationships.length === 0 ? No visible Relationships : relationships.map(edge => { const outgoing = edge.from === selectedId; const target = outgoing ? edge.to : edge.from; - const targetNode = graph.nodes.find(node => node.id === target); - const targetIsFile = targetNode?.nodeType !== 'folder'; return (
- ) :

Choose a File to inspect its incoming and outgoing Relationships.

} + ) :

Choose a Node to inspect its incoming and outgoing Relationships.

} ); diff --git a/apps/desktop/src/components/GraphSettingsPopover.test.tsx b/apps/desktop/src/components/GraphSettingsPopover.test.tsx index abb33d691..d8794a490 100644 --- a/apps/desktop/src/components/GraphSettingsPopover.test.tsx +++ b/apps/desktop/src/components/GraphSettingsPopover.test.tsx @@ -44,14 +44,14 @@ describe('GraphSettingsPopover', () => { const trigger = host.querySelector('[aria-label="Graph Settings"]'); await act(async () => trigger?.click()); - expect(host.querySelector('[role="dialog"]')).not.toBeNull(); + expect(host.querySelector('dialog')).not.toBeNull(); const repel = host.querySelector('#graph-setting-repelForce'); const center = host.querySelector('#graph-setting-centerForce'); const distance = host.querySelector('#graph-setting-linkDistance'); expect(document.activeElement).toBe(repel); expect([repel?.min, repel?.max, repel?.step]).toEqual(['0', '20', '1']); expect([center?.min, center?.max, center?.step]).toEqual(['0', '1', '0.01']); - expect([distance?.min, distance?.max, distance?.step]).toEqual(['30', '500', '10']); + expect([distance?.min, distance?.max, distance?.step]).toEqual(['30', '150', '10']); await act(async () => { if (!repel) return; @@ -66,7 +66,7 @@ describe('GraphSettingsPopover', () => { repel?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' })); }); await act(async () => undefined); - expect(host.querySelector('[role="dialog"]')).toBeNull(); + expect(host.querySelector('dialog')).toBeNull(); expect(document.activeElement).toBe(trigger); await act(async () => root.unmount()); }); diff --git a/apps/desktop/src/components/GraphSettingsPopover.tsx b/apps/desktop/src/components/GraphSettingsPopover.tsx index 98c065d35..15b8190d7 100644 --- a/apps/desktop/src/components/GraphSettingsPopover.tsx +++ b/apps/desktop/src/components/GraphSettingsPopover.tsx @@ -81,16 +81,16 @@ export function GraphSettingsPopover({ {open ? ( -
{ if (event.key !== 'Escape') return; event.preventDefault(); close(true); }} - role="dialog" >
@@ -135,7 +135,7 @@ export function GraphSettingsPopover({ ); })}
-
+ ) : null}
); diff --git a/apps/desktop/src/components/WorkspacePanes.test.tsx b/apps/desktop/src/components/WorkspacePanes.test.tsx new file mode 100644 index 000000000..1a6f67491 --- /dev/null +++ b/apps/desktop/src/components/WorkspacePanes.test.tsx @@ -0,0 +1,75 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DEFAULT_PANE_LAYOUT, PANE_LAYOUT_STORAGE_KEY } from '../paneLayout'; +import { WorkspacePanes } from './WorkspacePanes'; + +describe('Workspace pane splitters', () => { + const animationFrames: FrameRequestCallback[] = []; + const stored = new Map(); + const storage: Storage = { + get length() { return stored.size; }, + clear: () => stored.clear(), + getItem: key => stored.get(key) ?? null, + key: index => [...stored.keys()][index] ?? null, + removeItem: key => { stored.delete(key); }, + setItem: (key, value) => { stored.set(key, value); }, + }; + + beforeEach(() => { + vi.stubGlobal('localStorage', storage); + window.localStorage.clear(); + animationFrames.length = 0; + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + animationFrames.push(callback); + return animationFrames.length; + }); + vi.stubGlobal('ResizeObserver', class { + disconnect(): void {} + observe(): void {} + }); + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(1_200); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('renders two labeled vertical separators and persists keyboard resizing', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + Editor} + filesPane={} + graphPane={} + />, + )); + const separators = host.querySelectorAll('[role="separator"]'); + expect(separators).toHaveLength(2); + expect([...separators].map(separator => [ + separator.getAttribute('aria-label'), + separator.getAttribute('aria-orientation'), + ])).toEqual([ + ['Resize File hierarchy and editor panes', 'vertical'], + ['Resize editor and Relationship Graph panes', 'vertical'], + ]); + const before = host.querySelector('.workspace-grid')?.style.gridTemplateColumns; + await act(async () => { + separators[0]?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' })); + }); + await act(async () => animationFrames.splice(0).forEach(callback => callback(0))); + expect(host.querySelector('.workspace-grid')?.style.gridTemplateColumns).not.toBe(before); + expect(JSON.parse(window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY) ?? 'null')).not.toEqual(DEFAULT_PANE_LAYOUT); + + await act(async () => { + separators[1]?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); + }); + expect(JSON.parse(window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY) ?? 'null')).toEqual(DEFAULT_PANE_LAYOUT); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/components/WorkspacePanes.tsx b/apps/desktop/src/components/WorkspacePanes.tsx new file mode 100644 index 000000000..79b26be1a --- /dev/null +++ b/apps/desktop/src/components/WorkspacePanes.tsx @@ -0,0 +1,188 @@ +import { useEffect, useRef, useState } from 'react'; +import { + DEFAULT_PANE_LAYOUT, + MAX_FILES_PANE, + MIN_EDITOR_PANE, + MIN_FILES_PANE, + MIN_GRAPH_PANE, + PANE_LAYOUT_STORAGE_KEY, + PANE_SEPARATOR_WIDTH, + clampPaneLayout, + paneLayoutFromPixels, + readPaneLayout, + type PaneLayout, + type PanePixelLayout, +} from '../paneLayout'; + +type SeparatorKind = 'files' | 'graph'; + +interface PaneDrag { + kind: SeparatorKind; + layout: PanePixelLayout; + pointerId: number; + startX: number; +} + +function savePaneLayout(layout: PaneLayout): void { + try { + window.localStorage.setItem(PANE_LAYOUT_STORAGE_KEY, JSON.stringify(layout)); + } catch { + // The current layout remains usable when the webview denies local persistence. + } +} + +export function WorkspacePanes({ + editorPane, + filesPane, + graphPane, +}: { + editorPane: React.ReactNode; + filesPane: React.ReactNode; + graphPane: React.ReactNode; +}): React.ReactElement { + const [containerWidth, setContainerWidth] = useState(() => window.innerWidth); + const [layout, setLayout] = useState(() => readPaneLayout(window.localStorage)); + const [activeSeparator, setActiveSeparator] = useState(); + const containerRef = useRef(null); + const dragRef = useRef(); + const layoutRef = useRef(layout); + layoutRef.current = layout; + const pixels = clampPaneLayout(layout, containerWidth); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + const measure = (): void => setContainerWidth(container.clientWidth); + measure(); + const resize = new ResizeObserver(measure); + resize.observe(container); + return () => resize.disconnect(); + }, []); + + const updateFromPixels = (next: Pick): void => { + const normalized = clampPaneLayout(paneLayoutFromPixels(next, containerWidth), containerWidth); + setLayout(paneLayoutFromPixels(normalized, containerWidth)); + }; + + const commitLayout = (): void => savePaneLayout(layoutRef.current); + + const resetLayout = (): void => { + setLayout({ ...DEFAULT_PANE_LAYOUT }); + savePaneLayout(DEFAULT_PANE_LAYOUT); + }; + + const resizeBy = (kind: SeparatorKind, delta: number): void => { + if (kind === 'files') { + updateFromPixels({ files: pixels.files + delta, editor: pixels.editor - delta }); + } else { + updateFromPixels({ files: pixels.files, editor: pixels.editor + delta }); + } + }; + + const resizeToEdge = (kind: SeparatorKind, edge: 'minimum' | 'maximum'): void => { + if (kind === 'files') { + const files = edge === 'minimum' + ? MIN_FILES_PANE + : Math.min(MAX_FILES_PANE, pixels.files + pixels.editor - MIN_EDITOR_PANE); + updateFromPixels({ files, editor: pixels.editor + pixels.files - files }); + } else { + const editor = edge === 'minimum' + ? MIN_EDITOR_PANE + : pixels.editor + pixels.graph - MIN_GRAPH_PANE; + updateFromPixels({ files: pixels.files, editor }); + } + }; + + const separator = (kind: SeparatorKind, label: string): React.ReactElement => { + const value = kind === 'files' ? pixels.files : pixels.files + pixels.editor; + return ( +
{ + const step = event.shiftKey ? 32 : 10; + if (event.key === 'ArrowLeft') resizeBy(kind, -step); + else if (event.key === 'ArrowRight') resizeBy(kind, step); + else if (event.key === 'Home') resizeToEdge(kind, 'minimum'); + else if (event.key === 'End') resizeToEdge(kind, 'maximum'); + else if (event.key === 'Enter') resetLayout(); + else return; + event.preventDefault(); + if (event.key !== 'Enter') requestAnimationFrame(commitLayout); + }} + onPointerDown={(event) => { + if (event.button !== 0) return; + dragRef.current = { + kind, + layout: pixels, + pointerId: event.pointerId, + startX: event.clientX, + }; + setActiveSeparator(kind); + event.currentTarget.setPointerCapture(event.pointerId); + }} + onPointerMove={(event) => { + const drag = dragRef.current; + if (!drag || drag.kind !== kind || drag.pointerId !== event.pointerId) return; + const delta = event.clientX - drag.startX; + if (kind === 'files') { + updateFromPixels({ + files: drag.layout.files + delta, + editor: drag.layout.editor - delta, + }); + } else { + updateFromPixels({ files: drag.layout.files, editor: drag.layout.editor + delta }); + } + }} + onPointerUp={(event) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + dragRef.current = undefined; + setActiveSeparator(undefined); + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + requestAnimationFrame(commitLayout); + }} + onPointerCancel={(event) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + dragRef.current = undefined; + setActiveSeparator(undefined); + setLayout(paneLayoutFromPixels(drag.layout, containerWidth)); + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + }} + role="separator" + tabIndex={0} + title="Drag to resize. Double-click or press Enter to reset." + > + +
+ ); + }; + + return ( +
+ {filesPane} + {separator('files', 'Resize File hierarchy and editor panes')} + {editorPane} + {separator('graph', 'Resize editor and Relationship Graph panes')} + {graphPane} +
+ ); +} diff --git a/apps/desktop/src/components/createCodeEditor.test.ts b/apps/desktop/src/components/createCodeEditor.test.ts new file mode 100644 index 000000000..5d6f21854 --- /dev/null +++ b/apps/desktop/src/components/createCodeEditor.test.ts @@ -0,0 +1,41 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createCodeEditor } from './createCodeEditor'; + +describe('Code editor focus ownership', () => { + beforeEach(() => { + vi.stubGlobal('ResizeObserver', class { + disconnect(): void {} + observe(): void {} + }); + Object.defineProperties(Range.prototype, { + getBoundingClientRect: { configurable: true, value: () => new DOMRect() }, + getClientRects: { configurable: true, value: () => [] }, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('keeps hierarchy focus when a File creates a One Dark-highlighted editor', async () => { + const hierarchyItem = document.createElement('button'); + const editorHost = document.createElement('div'); + document.body.append(hierarchyItem, editorHost); + hierarchyItem.focus(); + const destroy = await createCodeEditor({ + content: 'export const answer = 42;', + onChange: () => undefined, + onSave: () => undefined, + parent: editorHost, + path: 'src/main.ts', + }); + + expect(document.activeElement).toBe(hierarchyItem); + expect(editorHost.querySelector('.cm-editor')).not.toBeNull(); + expect(editorHost.querySelector('.cm-line')?.textContent).toBe('export const answer = 42;'); + expect(document.head.textContent).toContain('#c678dd'); + + destroy(); + }); +}); diff --git a/apps/desktop/src/components/createCodeEditor.ts b/apps/desktop/src/components/createCodeEditor.ts index fc2b4ca6f..7d4700a97 100644 --- a/apps/desktop/src/components/createCodeEditor.ts +++ b/apps/desktop/src/components/createCodeEditor.ts @@ -16,12 +16,12 @@ export async function createCodeEditor({ { javascript }, { bracketMatching, - defaultHighlightStyle, indentOnInput, syntaxHighlighting, }, { highlightSelectionMatches, searchKeymap }, { EditorState }, + { oneDarkHighlightStyle }, { drawSelection, dropCursor, @@ -37,6 +37,7 @@ export async function createCodeEditor({ import('@codemirror/language'), import('@codemirror/search'), import('@codemirror/state'), + import('@codemirror/theme-one-dark'), import('@codemirror/view'), ]); const language = /\.[cm]?[jt]sx?$/iu.test(path) @@ -58,7 +59,7 @@ export async function createCodeEditor({ EditorState.allowMultipleSelections.of(true), indentOnInput(), bracketMatching(), - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + syntaxHighlighting(oneDarkHighlightStyle), highlightSelectionMatches(), highlightActiveLine(), language, @@ -74,24 +75,23 @@ export async function createCodeEditor({ if (update.docChanged) onChange(update.state.doc.toString()); }), EditorView.theme({ - '&': { height: '100%', backgroundColor: '#0c161d', color: '#dbe8ee' }, - '.cm-content': { caretColor: '#73e2c4', padding: '18px 0 40px' }, - '.cm-cursor, .cm-dropCursor': { borderLeftColor: '#73e2c4' }, + '&': { height: '100%', backgroundColor: 'var(--cg-editor)', color: '#abb2bf' }, + '.cm-content': { caretColor: 'var(--cg-accent)', padding: '18px 0 40px' }, + '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--cg-accent)' }, '.cm-gutters': { - backgroundColor: '#0c161d', - borderRight: '1px solid rgba(149, 180, 190, .1)', - color: '#49616d', + backgroundColor: 'var(--cg-editor-gutter)', + borderRight: '1px solid var(--cg-divider)', + color: 'var(--cg-text-quiet)', }, - '.cm-activeLine': { backgroundColor: 'rgba(110, 221, 193, .045)' }, - '.cm-activeLineGutter': { backgroundColor: 'rgba(110, 221, 193, .07)', color: '#88a5af' }, + '.cm-activeLine': { backgroundColor: 'var(--cg-editor-active-line)' }, + '.cm-activeLineGutter': { backgroundColor: 'var(--cg-editor-active-gutter)', color: 'var(--cg-text-secondary)' }, '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { - backgroundColor: 'rgba(69, 141, 168, .35)', + backgroundColor: 'var(--cg-editor-selection)', }, - '.cm-scroller': { fontFamily: 'var(--font-code)', fontSize: '12.5px', lineHeight: '1.68' }, + '.cm-scroller': { fontFamily: 'var(--cg-font-mono)', fontSize: '12.5px', lineHeight: '1.68' }, }), ], }), }); - view.focus(); return () => view.destroy(); } diff --git a/apps/desktop/src/desktopGraphInteraction.test.ts b/apps/desktop/src/desktopGraphInteraction.test.ts index 5b0fcc0e0..821b72cf7 100644 --- a/apps/desktop/src/desktopGraphInteraction.test.ts +++ b/apps/desktop/src/desktopGraphInteraction.test.ts @@ -3,6 +3,7 @@ import { passedDesktopGraphDragThreshold, pickDesktopGraphNode, screenToDesktopGraph, + zoomDesktopGraphBy, zoomDesktopGraphAtPointer, } from './desktopGraphInteraction'; @@ -25,6 +26,14 @@ describe('desktop graph interaction adapter', () => { expect(zoomed.zoom).toBeGreaterThan(camera.zoom); }); + it('zooms controls around the current center and clamps the viewport range', () => { + const camera = { centerX: 20, centerY: -10, zoom: 1 }; + + expect(zoomDesktopGraphBy(camera, 1.2)).toEqual({ centerX: 20, centerY: -10, zoom: 1.2 }); + expect(zoomDesktopGraphBy(camera, 100)).toEqual({ centerX: 20, centerY: -10, zoom: 5 }); + expect(zoomDesktopGraphBy(camera, 0.001)).toEqual({ centerX: 20, centerY: -10, zoom: 0.05 }); + }); + it('uses the extension three-pixel cumulative drag threshold', () => { expect(passedDesktopGraphDragThreshold(10, 10, 13, 10)).toBe(false); expect(passedDesktopGraphDragThreshold(10, 10, 13.1, 10)).toBe(true); diff --git a/apps/desktop/src/desktopGraphInteraction.ts b/apps/desktop/src/desktopGraphInteraction.ts index a08c27713..66b3f0228 100644 --- a/apps/desktop/src/desktopGraphInteraction.ts +++ b/apps/desktop/src/desktopGraphInteraction.ts @@ -41,6 +41,19 @@ export function zoomDesktopGraphAtPointer( }; } +export function zoomDesktopGraphBy( + camera: GraphRendererCamera, + factor: number, +): GraphRendererCamera { + return { + ...camera, + zoom: Math.min( + DESKTOP_GRAPH_MAX_ZOOM, + Math.max(DESKTOP_GRAPH_MIN_ZOOM, camera.zoom * factor), + ), + }; +} + export function passedDesktopGraphDragThreshold( originX: number, originY: number, diff --git a/apps/desktop/src/fileTreeModel.test.ts b/apps/desktop/src/fileTreeModel.test.ts new file mode 100644 index 000000000..2241c0420 --- /dev/null +++ b/apps/desktop/src/fileTreeModel.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; +import type { FileTreeEntry } from './model'; +import { filterFileTree, flattenVisibleFileTree } from './fileTreeModel'; + +const entries: FileTreeEntry[] = [ + { + kind: 'folder', + name: 'src', + path: 'src', + children: [ + { kind: 'file', name: 'main.ts', path: 'src/main.ts' }, + { + kind: 'folder', + name: 'graph', + path: 'src/graph', + children: [{ kind: 'file', name: 'camera.ts', path: 'src/graph/camera.ts' }], + }, + ], + }, + { kind: 'file', name: 'package.json', path: 'package.json' }, +]; + +describe('File hierarchy model', () => { + it('filters by relative path while keeping matching ancestors', () => { + expect(filterFileTree(entries, 'graph/cam')).toEqual([{ + kind: 'folder', + name: 'src', + path: 'src', + children: [{ + kind: 'folder', + name: 'graph', + path: 'src/graph', + children: [{ kind: 'file', name: 'camera.ts', path: 'src/graph/camera.ts' }], + }], + }]); + }); + + it('flattens only expanded hierarchy rows in keyboard order', () => { + expect(flattenVisibleFileTree(entries, new Set(['src']), false).map(row => [ + row.entry.path, + row.parentPath, + ])).toEqual([ + ['src', undefined], + ['src/main.ts', 'src'], + ['src/graph', 'src'], + ['package.json', undefined], + ]); + }); + + it('forces matching result ancestors open without mutating collapse state', () => { + const filtered = filterFileTree(entries, 'camera'); + expect(flattenVisibleFileTree(filtered, new Set(), true).map(row => row.entry.path)).toEqual([ + 'src', + 'src/graph', + 'src/graph/camera.ts', + ]); + }); +}); diff --git a/apps/desktop/src/fileTreeModel.ts b/apps/desktop/src/fileTreeModel.ts new file mode 100644 index 000000000..20c0b660a --- /dev/null +++ b/apps/desktop/src/fileTreeModel.ts @@ -0,0 +1,56 @@ +import type { FileTreeEntry } from './model'; + +export interface VisibleFileTreeItem { + depth: number; + entry: FileTreeEntry; + parentPath?: string; +} + +function normalizedQuery(query: string): string { + return query.trim().toLocaleLowerCase(); +} + +function filterEntry(entry: FileTreeEntry, query: string): FileTreeEntry | undefined { + const matches = entry.name.toLocaleLowerCase().includes(query) + || entry.path.toLocaleLowerCase().includes(query); + if (entry.kind === 'file') return matches ? entry : undefined; + const children = entry.children + .map(child => filterEntry(child, query)) + .filter(child => child !== undefined); + return matches || children.length > 0 ? { ...entry, children } : undefined; +} + +export function filterFileTree(entries: FileTreeEntry[], query: string): FileTreeEntry[] { + const normalized = normalizedQuery(query); + if (!normalized) return entries; + return entries + .map(entry => filterEntry(entry, normalized)) + .filter(entry => entry !== undefined); +} + +export function collectFolderPaths(entries: FileTreeEntry[]): Set { + const paths = new Set(); + const visit = (entry: FileTreeEntry): void => { + if (entry.kind === 'file') return; + paths.add(entry.path); + for (const child of entry.children) visit(child); + }; + for (const entry of entries) visit(entry); + return paths; +} + +export function flattenVisibleFileTree( + entries: FileTreeEntry[], + expandedPaths: ReadonlySet, + forceExpanded: boolean, +): VisibleFileTreeItem[] { + const visible: VisibleFileTreeItem[] = []; + const visit = (entry: FileTreeEntry, depth: number, parentPath?: string): void => { + visible.push({ depth, entry, ...(parentPath ? { parentPath } : {}) }); + if (entry.kind === 'folder' && (forceExpanded || expandedPaths.has(entry.path))) { + for (const child of entry.children) visit(child, depth + 1, entry.path); + } + }; + for (const entry of entries) visit(entry, 1); + return visible; +} diff --git a/apps/desktop/src/model.test.ts b/apps/desktop/src/model.test.ts index 6b237baef..0331b8b96 100644 --- a/apps/desktop/src/model.test.ts +++ b/apps/desktop/src/model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildFileTree, parseWorkspaceGraphResult } from './model'; +import { buildFileTree, formatGraphCounts, parseWorkspaceGraphResult } from './model'; const graphResult = { kind: 'ready', @@ -20,6 +20,19 @@ const graphResult = { } satisfies unknown; describe('desktop workspace graph model', () => { + it('counts displayed File and Folder Nodes plus every displayed Relationship', () => { + expect(formatGraphCounts({ + nodes: [ + { id: 'src', label: 'src', nodeType: 'folder' }, + { id: 'src/index.ts', label: 'index.ts', nodeType: 'file' }, + ], + edges: [ + { id: 'a', from: 'src', to: 'src/index.ts', kind: 'nests' }, + { id: 'b', from: 'src', to: 'src/index.ts', kind: 'contains' }, + ], + })).toBe('2 Nodes · 2 Relationships'); + }); + it('parses a File and Folder-only graph and builds the File hierarchy', () => { const result = parseWorkspaceGraphResult(graphResult); diff --git a/apps/desktop/src/model.ts b/apps/desktop/src/model.ts index 4df6e0077..c85e7bbf9 100644 --- a/apps/desktop/src/model.ts +++ b/apps/desktop/src/model.ts @@ -186,3 +186,7 @@ export function buildFileTree(graph: DesktopGraph): FileTreeEntry[] { export function countFiles(graph: DesktopGraph): number { return graph.nodes.filter(node => node.nodeType !== 'folder').length; } + +export function formatGraphCounts(graph: DesktopGraph): string { + return `${graph.nodes.length} Nodes · ${graph.edges.length} Relationships`; +} diff --git a/apps/desktop/src/paneLayout.test.ts b/apps/desktop/src/paneLayout.test.ts new file mode 100644 index 000000000..b5f1c2f11 --- /dev/null +++ b/apps/desktop/src/paneLayout.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_PANE_LAYOUT, + PANE_LAYOUT_STORAGE_KEY, + clampPaneLayout, + parsePaneLayout, + readPaneLayout, +} from './paneLayout'; + +describe('desktop pane layout', () => { + it('parses one exact finite interface record', () => { + expect(parsePaneLayout({ files: 0.2, editor: 0.4 })).toEqual({ files: 0.2, editor: 0.4 }); + expect(parsePaneLayout({ files: 0.2, editor: 0.4, extra: true })).toEqual(DEFAULT_PANE_LAYOUT); + expect(parsePaneLayout({ files: Number.NaN, editor: 0.4 })).toEqual(DEFAULT_PANE_LAYOUT); + }); + + it('restores defaults for missing or malformed local data', () => { + expect(readPaneLayout({ getItem: () => null })).toEqual(DEFAULT_PANE_LAYOUT); + expect(readPaneLayout({ getItem: key => key === PANE_LAYOUT_STORAGE_KEY ? '{bad' : null })) + .toEqual(DEFAULT_PANE_LAYOUT); + }); + + it('clamps every pane to a usable width as the window changes', () => { + expect(clampPaneLayout({ files: 0.9, editor: 0.09 }, 1_040)).toEqual({ + files: 388, + editor: 320, + graph: 320, + }); + const wide = clampPaneLayout({ files: 0.16, editor: 0.45 }, 1_440); + expect(wide.files).toBeGreaterThanOrEqual(180); + expect(wide.editor).toBeGreaterThanOrEqual(320); + expect(wide.graph).toBeGreaterThanOrEqual(320); + expect(wide.files + wide.editor + wide.graph).toBe(1_428); + }); +}); diff --git a/apps/desktop/src/paneLayout.ts b/apps/desktop/src/paneLayout.ts new file mode 100644 index 000000000..80d0e993f --- /dev/null +++ b/apps/desktop/src/paneLayout.ts @@ -0,0 +1,71 @@ +export interface PaneLayout { + editor: number; + files: number; +} + +export interface PanePixelLayout { + editor: number; + files: number; + graph: number; +} + +export const DEFAULT_PANE_LAYOUT: PaneLayout = { files: 0.16, editor: 0.45 }; +export const PANE_LAYOUT_STORAGE_KEY = 'codegraphy.desktop.pane-layout'; +export const PANE_SEPARATOR_WIDTH = 6; +export const MIN_FILES_PANE = 180; +export const MAX_FILES_PANE = 420; +export const MIN_EDITOR_PANE = 320; +export const MIN_GRAPH_PANE = 320; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function parsePaneLayout(value: unknown): PaneLayout { + if (!isRecord(value) + || Object.keys(value).sort().join(',') !== 'editor,files' + || typeof value.files !== 'number' + || !Number.isFinite(value.files) + || typeof value.editor !== 'number' + || !Number.isFinite(value.editor) + || value.files <= 0 + || value.editor <= 0 + || value.files + value.editor >= 1) { + return { ...DEFAULT_PANE_LAYOUT }; + } + return { files: value.files, editor: value.editor }; +} + +export function readPaneLayout(storage: Pick): PaneLayout { + const stored = storage.getItem(PANE_LAYOUT_STORAGE_KEY); + if (stored === null) return { ...DEFAULT_PANE_LAYOUT }; + try { + return parsePaneLayout(JSON.parse(stored)); + } catch { + return { ...DEFAULT_PANE_LAYOUT }; + } +} + +export function clampPaneLayout(layout: PaneLayout, containerWidth: number): PanePixelLayout { + const usableWidth = Math.max( + MIN_FILES_PANE + MIN_EDITOR_PANE + MIN_GRAPH_PANE, + containerWidth - PANE_SEPARATOR_WIDTH * 2, + ); + const files = Math.min( + Math.min(MAX_FILES_PANE, usableWidth - MIN_EDITOR_PANE - MIN_GRAPH_PANE), + Math.max(MIN_FILES_PANE, layout.files * usableWidth), + ); + const editor = Math.min( + usableWidth - files - MIN_GRAPH_PANE, + Math.max(MIN_EDITOR_PANE, layout.editor * usableWidth), + ); + return { files, editor, graph: usableWidth - files - editor }; +} + +export function paneLayoutFromPixels( + layout: Pick, + containerWidth: number, +): PaneLayout { + const usableWidth = Math.max(1, containerWidth - PANE_SEPARATOR_WIDTH * 2); + return { files: layout.files / usableWidth, editor: layout.editor / usableWidth }; +} diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index ed87e3195..90a966cef 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -1,34 +1,38 @@ :root { color-scheme: dark; - --cg-bg: #0b0d0f; - --cg-toolbar: #111417; - --cg-sidebar: #0f1215; - --cg-editor: #111519; - --cg-graph: #0d1317; - --cg-inspector: #101417; - --cg-popover: #191d21; - --cg-surface-hover: #1a2024; - --cg-surface-active: #18302e; - --cg-border: #262c30; - --cg-border-strong: #394247; - --cg-divider: rgba(170, 185, 191, 0.14); - --cg-text: #e6ebed; - --cg-text-secondary: #a8b2b6; - --cg-text-muted: #7f8b90; - --cg-text-quiet: #748086; - --cg-accent: #68d5bc; - --cg-accent-hover: #7ee2ca; - --cg-accent-surface: rgba(104, 213, 188, 0.12); - --cg-focus: #8ce8d2; - --cg-danger: #ff858b; - --cg-warning: #e8bd6a; + --cg-bg: #21252b; + --cg-toolbar: #282c34; + --cg-sidebar: #252930; + --cg-editor: #282c34; + --cg-editor-gutter: #252930; + --cg-editor-active-line: rgba(97, 175, 239, 0.055); + --cg-editor-active-gutter: rgba(97, 175, 239, 0.09); + --cg-editor-selection: rgba(82, 139, 255, 0.32); + --cg-graph: #242830; + --cg-inspector: #272b33; + --cg-popover: #2c313a; + --cg-surface-hover: #303641; + --cg-surface-active: #30433f; + --cg-border: #3a404b; + --cg-border-strong: #515a68; + --cg-divider: rgba(171, 178, 191, 0.15); + --cg-text: #e5e9f0; + --cg-text-secondary: #c3c9d4; + --cg-text-muted: #9ca5b4; + --cg-text-quiet: #8a94a5; + --cg-accent: #5bc8ad; + --cg-accent-hover: #70d8bd; + --cg-accent-surface: rgba(91, 200, 173, 0.13); + --cg-focus: #61afef; + --cg-danger: #e8838b; + --cg-warning: #e5c07b; --cg-graph-background: var(--cg-graph); --cg-graph-label-foreground: var(--cg-text); --cg-graph-label-muted-foreground: var(--cg-text-muted); --cg-graph-link-highlight: var(--cg-accent); - --cg-graph-link-muted: #455158; + --cg-graph-link-muted: #596373; --cg-graph-node-selection-border: var(--cg-focus); - --cg-shadow: 0 18px 50px rgba(0, 0, 0, 0.48), 0 2px 10px rgba(0, 0, 0, 0.32); + --cg-shadow: 0 18px 50px rgba(8, 10, 14, 0.48), 0 2px 10px rgba(8, 10, 14, 0.32); --cg-font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; --cg-font-mono: "SFMono-Regular", "SF Mono", Menlo, Monaco, Consolas, monospace; font-family: var(--cg-font-ui); @@ -79,8 +83,8 @@ button:focus-visible, grid-template-columns: minmax(230px, 1fr) auto minmax(230px, 1fr); align-items: center; border-bottom: 1px solid var(--cg-border); - background: rgba(17, 20, 23, 0.96); - box-shadow: 0 1px rgba(255, 255, 255, 0.02) inset; + background: color-mix(in srgb, var(--cg-toolbar) 96%, transparent); + box-shadow: 0 1px rgba(255, 255, 255, 0.025) inset; user-select: none; -webkit-backdrop-filter: blur(18px) saturate(115%); backdrop-filter: blur(18px) saturate(115%); @@ -125,7 +129,7 @@ button:focus-visible, min-height: 28px; border: 1px solid var(--cg-border); border-radius: 6px; - background: #171b1e; + background: #2c313a; color: var(--cg-text-secondary); padding: 0 10px; font-size: 10.5px; @@ -135,12 +139,12 @@ button:focus-visible, .toolbar-button:hover:not(:disabled), .save-button:hover:not(:disabled) { border-color: var(--cg-border-strong); - background: #1d2226; + background: #353b46; color: var(--cg-text); } .toolbar-button:active:not(:disabled), -.save-button:active:not(:disabled) { background: #121619; } +.save-button:active:not(:disabled) { background: #282d35; } .toolbar-button.is-active { border-color: rgba(104, 213, 188, 0.42); background: var(--cg-accent-surface); color: var(--cg-accent); } .toolbar-button:disabled, .save-button:disabled { color: var(--cg-text-quiet); opacity: 0.56; } @@ -166,7 +170,7 @@ button:focus-visible, } .workspace-trigger:hover, -.workspace-trigger[aria-expanded="true"] { border-color: var(--cg-border); background: #181c20; } +.workspace-trigger[aria-expanded="true"] { border-color: var(--cg-border); background: #303641; } .workspace-trigger-icon { width: 12px; height: 9px; border: 1px solid #718086; border-radius: 2px; box-shadow: 0 -3px 0 -1px #718086; } .workspace-trigger-copy { display: grid; min-width: 0; gap: 1px; } .workspace-trigger-label { color: var(--cg-text-muted); font-size: 8px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; } @@ -184,7 +188,7 @@ button:focus-visible, overflow: auto; border: 1px solid var(--cg-border-strong); border-radius: 9px; - background: rgba(25, 29, 33, 0.98); + background: color-mix(in srgb, var(--cg-popover) 98%, transparent); padding: 5px; box-shadow: var(--cg-shadow); transform: translateX(-50%); @@ -222,8 +226,8 @@ button:focus-visible, .workspace-grid { display: grid; - grid-template-columns: minmax(190px, 16%) minmax(360px, 45%) minmax(380px, 39%); min-height: 0; + overflow: hidden; } .files-pane, @@ -235,10 +239,40 @@ button:focus-visible, min-height: 0; } -.files-pane { border-right: 1px solid var(--cg-border); background: var(--cg-sidebar); } -.editor-pane { border-right: 1px solid var(--cg-border); background: var(--cg-editor); } +.files-pane { background: var(--cg-sidebar); } +.editor-pane { background: var(--cg-editor); } .graph-pane { background: var(--cg-graph); } +.pane-separator { + position: relative; + z-index: 12; + display: grid; + width: 6px; + min-width: 6px; + place-items: stretch center; + border: 0; + background: var(--cg-bg); + cursor: col-resize; + touch-action: none; + user-select: none; +} + +.pane-separator span { + width: 1px; + background: var(--cg-border); + transition: width 100ms ease, background-color 100ms ease, box-shadow 100ms ease; +} + +.pane-separator:hover span, +.pane-separator.is-active span, +.pane-separator:focus-visible span { + width: 2px; + background: var(--cg-accent); + box-shadow: 0 0 0 1px rgba(91, 200, 173, 0.16); +} + +.pane-separator:focus-visible { outline: 2px solid var(--cg-focus); outline-offset: -2px; } + .pane-heading { display: flex; align-items: center; @@ -258,6 +292,22 @@ button:focus-visible, .editor-heading > span:first-child { overflow: hidden; color: #c6d0d4; font-family: var(--cg-font-mono); font-size: 9.5px; font-weight: 450; letter-spacing: 0; text-overflow: ellipsis; text-transform: none; white-space: nowrap; } .graph-heading-actions { display: flex; align-items: center; gap: 7px; } +.editor-heading-actions { display: flex; align-items: center; gap: 5px; } +.close-file-button { + display: grid; + width: 23px; + height: 23px; + place-items: center; + border: 1px solid transparent; + border-radius: 5px; + background: transparent; + padding: 0; + color: var(--cg-text-muted); + font-size: 16px; + line-height: 1; +} +.close-file-button:hover:not(:disabled) { border-color: var(--cg-border); background: var(--cg-surface-hover); color: var(--cg-text); } +.close-file-button:disabled { opacity: 0.42; } .graph-settings { position: relative; } .graph-settings-trigger { display: grid; @@ -285,9 +335,10 @@ button:focus-visible, right: -5px; z-index: 55; width: 278px; + margin: 0; border: 1px solid var(--cg-border-strong); border-radius: 9px; - background: rgba(25, 29, 33, 0.98); + background: color-mix(in srgb, var(--cg-popover) 98%, transparent); padding: 11px; color: var(--cg-text); box-shadow: var(--cg-shadow); @@ -310,7 +361,38 @@ button:focus-visible, .graph-settings-control input[type="range"] { width: 100%; height: 15px; margin: 0; accent-color: var(--cg-accent); } .graph-settings-control > span { color: var(--cg-text-quiet); font-size: 8.5px; line-height: 1.35; } -.file-tree { min-height: 0; overflow: auto; padding: 6px 5px 30px; scrollbar-color: #3a4449 transparent; scrollbar-width: thin; } +.file-browser { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } +.file-filter { + display: grid; + grid-template-columns: 14px minmax(0, 1fr) auto; + align-items: center; + min-width: 0; + gap: 5px; + margin: 7px 7px 3px; + border: 1px solid var(--cg-border); + border-radius: 6px; + background: #20242b; + padding: 0 7px; + color: var(--cg-text-muted); +} +.file-filter:focus-within { border-color: var(--cg-focus); box-shadow: 0 0 0 1px color-mix(in srgb, var(--cg-focus) 28%, transparent); } +.file-filter-icon { font-size: 13px; transform: translateY(-1px); } +.file-filter input { + width: 100%; + height: 26px; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + color: var(--cg-text); + font-family: var(--cg-font-ui); + font-size: 10px; +} +.file-filter input::placeholder { color: var(--cg-text-quiet); } +.file-filter input::-webkit-search-cancel-button { filter: invert(0.72); } +.file-filter kbd { color: var(--cg-text-quiet); font-family: var(--cg-font-ui); font-size: 8px; } +.file-filter-empty { margin: 18px 14px; color: var(--cg-text-muted); font-size: 10px; line-height: 1.5; } +.file-tree { min-height: 0; overflow: auto; padding: 4px 5px 30px; scrollbar-color: #4b5360 transparent; scrollbar-width: thin; } .tree-item { font-family: var(--cg-font-mono); } .tree-file, .tree-folder-button { @@ -324,16 +406,16 @@ button:focus-visible, border-radius: 4px; background: transparent; padding: 0 7px; - color: #9ca8ad; + color: var(--cg-text-muted); font-size: 10px; text-align: left; white-space: nowrap; } -.tree-folder-button { padding-left: 3px; color: #b1bbbf; } +.tree-folder-button { padding-left: 3px; color: var(--cg-text-secondary); } .tree-file:hover, .tree-folder-button:hover { background: var(--cg-surface-hover); color: var(--cg-text); } -.tree-file.is-selected { background: var(--cg-surface-active); color: #e8f7f3; box-shadow: 2px 0 0 var(--cg-accent) inset; } +.tree-file.is-selected { background: var(--cg-surface-active); color: var(--cg-text); box-shadow: 2px 0 0 var(--cg-accent) inset; } .tree-item > span:last-child { overflow: hidden; text-overflow: ellipsis; } .folder-caret { width: 10px; flex: 0 0 auto; color: var(--cg-text-quiet); font-family: var(--cg-font-ui); font-size: 14px; line-height: 1; transform: rotate(0deg); transition: transform 100ms ease; } .tree-folder.is-expanded > .tree-folder-button .folder-caret { transform: rotate(90deg); } @@ -346,35 +428,60 @@ button:focus-visible, .editor-host { min-height: 0; height: 100%; overflow: hidden; } .editor-host .cm-editor { height: 100%; } .editor-state { display: grid; height: 100%; place-content: center; padding: 32px; color: var(--cg-text-muted); font-size: 11px; text-align: center; } -.editor-state--error { color: #ffb3b7; } +.editor-state--error { color: #f0a3aa; } .save-button { min-height: 24px; padding: 0 8px; font-size: 9px; font-weight: 550; letter-spacing: 0; text-transform: none; } .save-button.has-changes { border-color: rgba(104, 213, 188, 0.46); background: var(--cg-accent-surface); color: var(--cg-accent-hover); } .empty-editor { display: grid; min-height: 0; place-content: center; justify-items: center; padding: 40px; text-align: center; } -.empty-glyph { color: #415159; font-family: var(--cg-font-mono); font-size: 42px; letter-spacing: -0.12em; } +.empty-glyph { color: #596273; font-family: var(--cg-font-mono); font-size: 42px; letter-spacing: -0.12em; } .empty-editor h2 { margin: 17px 0 6px; font-size: 16px; font-weight: 540; letter-spacing: -0.02em; } .empty-editor p { max-width: 330px; margin: 0; color: var(--cg-text-muted); font-size: 11.5px; line-height: 1.55; } .graph-panel { display: grid; grid-template-rows: minmax(0, 1fr) minmax(132px, 25%); min-height: 0; } -.graph-canvas-wrap { position: relative; min-height: 0; overflow: hidden; background-color: var(--cg-graph); background-image: linear-gradient(rgba(145, 170, 179, 0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(145, 170, 179, 0.035) 1px, transparent 1px); background-size: 24px 24px; } +.graph-canvas-wrap { position: relative; min-height: 0; overflow: hidden; background-color: var(--cg-graph); background-image: linear-gradient(rgba(171, 178, 191, 0.045) 1px, transparent 1px), linear-gradient(90deg, rgba(171, 178, 191, 0.045) 1px, transparent 1px); background-size: 24px 24px; } .graph-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; cursor: grab; } .graph-canvas:active { cursor: grabbing; } .graph-canvas-overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; } .graph-error { position: absolute; inset: auto 12px 12px; border: 1px solid rgba(255, 133, 139, 0.42); border-radius: 6px; background: rgba(66, 25, 29, 0.96); padding: 9px 11px; color: #ffc2c5; font-size: 10px; box-shadow: var(--cg-shadow); } -.graph-legend { position: absolute; top: 9px; right: 9px; display: flex; gap: 12px; border: 1px solid var(--cg-border); border-radius: 6px; background: rgba(20, 25, 28, 0.86); padding: 6px 8px; color: var(--cg-text-muted); font-size: 8.5px; -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); } -.graph-legend span { display: flex; align-items: center; gap: 5px; } -.graph-legend i { width: 8px; height: 8px; border-radius: 50%; } -.legend-file { border: 1px solid #a6e7d7; background: #197c84; } -.legend-folder { border: 1px solid #8ea0a6; background: transparent; } +.graph-viewport-controls { + position: absolute; + top: 9px; + right: 9px; + display: flex; + overflow: hidden; + border: 1px solid var(--cg-border); + border-radius: 7px; + background: color-mix(in srgb, var(--cg-popover) 88%, transparent); + box-shadow: 0 4px 16px rgba(8, 10, 14, 0.24); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); +} +.graph-viewport-controls button { + display: grid; + width: 27px; + height: 27px; + place-items: center; + border: 0; + border-right: 1px solid var(--cg-divider); + background: transparent; + padding: 0; + color: var(--cg-text-secondary); + font-family: var(--cg-font-ui); + font-size: 15px; +} +.graph-viewport-controls button:last-child { border-right: 0; } +.graph-viewport-controls button:hover { background: var(--cg-surface-hover); color: var(--cg-text); } +.graph-viewport-controls button:active { background: var(--cg-surface-active); } +.fit-screen-icon { font-size: 13px; transform: rotate(45deg); } .relationship-inspector { min-height: 0; overflow: auto; border-top: 1px solid var(--cg-border); background: var(--cg-inspector); padding: 10px 11px 16px; scrollbar-color: #3a4449 transparent; scrollbar-width: thin; } .inspector-heading { display: flex; justify-content: space-between; gap: 12px; color: var(--cg-text-muted); font-size: 8.5px; font-weight: 620; letter-spacing: 0.07em; text-transform: uppercase; } .inspector-heading span:last-child { color: var(--cg-text-quiet); font-family: var(--cg-font-mono); font-weight: 450; letter-spacing: 0; text-transform: none; } -.selected-node-title { display: block; overflow: hidden; margin-top: 9px; color: #d4dcdf; font-family: var(--cg-font-mono); font-size: 9.5px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } +.selected-node-title { display: block; overflow: hidden; margin-top: 9px; color: var(--cg-text); font-family: var(--cg-font-mono); font-size: 9.5px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } .relationship-list { display: grid; gap: 1px; margin-top: 6px; } -.relationship-list button { display: grid; grid-template-columns: 68px 13px minmax(0, 1fr); align-items: center; min-height: 25px; border: 0; border-radius: 4px; background: transparent; padding: 0 6px; color: #98a6ab; font-family: var(--cg-font-mono); font-size: 9px; text-align: left; } +.relationship-list button { display: grid; grid-template-columns: 68px 13px minmax(0, 1fr); align-items: center; min-height: 25px; border: 0; border-radius: 4px; background: transparent; padding: 0 6px; color: var(--cg-text-muted); font-family: var(--cg-font-mono); font-size: 9px; text-align: left; } .relationship-list button:hover:not(:disabled) { background: var(--cg-surface-hover); color: var(--cg-text); } .relationship-list button:disabled { color: var(--cg-text-quiet); opacity: 0.64; } .relationship-list button:disabled .relationship-kind { color: #61716f; } -.relationship-kind { overflow: hidden; color: #62c7b0; text-overflow: ellipsis; } +.relationship-kind { overflow: hidden; color: var(--cg-accent); text-overflow: ellipsis; } .relationship-direction { color: var(--cg-text-quiet); } .relationship-list button span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .quiet { color: var(--cg-text-quiet); font-size: 9.5px; line-height: 1.5; } @@ -390,16 +497,16 @@ button:focus-visible, text-align: center; } -.welcome-mark { display: grid; width: 88px; height: 88px; place-items: center; border: 1px solid var(--cg-border-strong); border-radius: 22px; background: #111518; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.34); } +.welcome-mark { display: grid; width: 88px; height: 88px; place-items: center; border: 1px solid var(--cg-border-strong); border-radius: 22px; background: var(--cg-toolbar); box-shadow: 0 20px 60px rgba(8, 10, 14, 0.34); } .welcome-mark img { width: 58px; height: 58px; object-fit: contain; } .eyebrow { margin: 25px 0 13px; color: var(--cg-accent); font-family: var(--cg-font-mono); font-size: 8.5px; font-weight: 600; letter-spacing: 0.16em; } -.welcome-state h1 { margin: 0; color: #f1f4f5; font-size: clamp(34px, 5vw, 58px); font-weight: 520; line-height: 1.04; letter-spacing: -0.047em; } +.welcome-state h1 { margin: 0; color: var(--cg-text); font-size: clamp(34px, 5vw, 58px); font-weight: 520; line-height: 1.04; letter-spacing: -0.047em; } .welcome-state > p:not(.eyebrow) { max-width: 540px; margin: 19px 0 0; color: var(--cg-text-secondary); font-size: 12.5px; line-height: 1.62; } .welcome-state > button { min-height: 39px; margin-top: 25px; border: 1px solid #73d8c1; border-radius: 7px; background: #236f61; padding: 0 17px; color: #f3fffc; font-size: 11.5px; font-weight: 580; box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28); } .welcome-state > button:hover { background: #2b7f6f; } .welcome-state > button:active { background: #1d6256; } -.status-bar { display: flex; align-items: center; min-width: 0; gap: 7px; border-top: 1px solid var(--cg-border); background: #0c0f11; padding: 0 8px; color: var(--cg-text-muted); font-family: var(--cg-font-mono); font-size: 8.5px; } +.status-bar { display: flex; align-items: center; min-width: 0; gap: 7px; border-top: 1px solid var(--cg-border); background: #20242a; padding: 0 8px; color: var(--cg-text-muted); font-family: var(--cg-font-mono); font-size: 8.5px; } .status-dot { width: 5px; height: 5px; flex: 0 0 auto; border-radius: 50%; background: #627177; } .status-ready .status-dot { background: var(--cg-accent); box-shadow: 0 0 0 2px rgba(104, 213, 188, 0.1); } .status-busy .status-dot { background: var(--cg-warning); animation: status-pulse 1s ease-in-out infinite; } @@ -412,14 +519,14 @@ button:focus-visible, width: min(430px, calc(100vw - 32px)); border: 1px solid var(--cg-border-strong); border-radius: 12px; - background: #1a1e22; + background: var(--cg-popover); padding: 24px; color: var(--cg-text); box-shadow: var(--cg-shadow); } .workspace-switch-dialog::backdrop { background: rgba(3, 5, 6, 0.64); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); } -.dialog-file-glyph { display: grid; width: 36px; height: 36px; place-items: center; border: 1px solid var(--cg-border-strong); border-radius: 8px; background: #111518; color: var(--cg-accent); font-family: var(--cg-font-mono); font-size: 13px; } +.dialog-file-glyph { display: grid; width: 36px; height: 36px; place-items: center; border: 1px solid var(--cg-border-strong); border-radius: 8px; background: var(--cg-toolbar); color: var(--cg-accent); font-family: var(--cg-font-mono); font-size: 13px; } .workspace-switch-dialog h2 { margin: 18px 0 7px; font-size: 16px; font-weight: 610; letter-spacing: -0.02em; } .workspace-switch-dialog p { margin: 0; color: var(--cg-text-secondary); font-size: 11px; line-height: 1.55; } .workspace-switch-dialog p strong { overflow-wrap: anywhere; color: #d9e1e3; font-family: var(--cg-font-mono); font-size: 10px; font-weight: 500; } @@ -436,7 +543,6 @@ button:focus-visible, .app-toolbar { grid-template-columns: minmax(200px, 1fr) auto minmax(130px, 1fr); } .brand-copy span { display: none; } .workspace-trigger { width: clamp(210px, 28vw, 300px); } - .workspace-grid { grid-template-columns: 185px minmax(330px, 1fr) minmax(350px, 0.92fr); } } @media (max-width: 900px) { @@ -445,7 +551,6 @@ button:focus-visible, .brand-copy { display: none; } .workspace-trigger { width: 100%; } .toolbar-button { padding: 0 8px; } - .workspace-grid { grid-template-columns: 172px minmax(320px, 1fr) minmax(320px, 1fr); overflow-x: auto; } } @media (prefers-reduced-motion: reduce) { @@ -484,8 +589,7 @@ button:focus-visible, .tree-file.is-selected, .workspace-trigger[aria-expanded="true"] { background: Highlight; color: HighlightText; } .status-dot, - .legend-file, - .legend-folder { border: 1px solid CanvasText; background: CanvasText; box-shadow: none; } + .pane-separator span { border: 1px solid CanvasText; background: CanvasText; box-shadow: none; } button:focus-visible, [role="treeitem"]:focus-visible, .graph-canvas:focus-visible { outline-color: Highlight; } diff --git a/apps/desktop/src/useDesktopGraphRenderer.ts b/apps/desktop/src/useDesktopGraphRenderer.ts index 227acbc1a..8f5c38e90 100644 --- a/apps/desktop/src/useDesktopGraphRenderer.ts +++ b/apps/desktop/src/useDesktopGraphRenderer.ts @@ -22,14 +22,13 @@ import { toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, } from '@codegraphy-dev/graph-visuals'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { DESKTOP_GRAPH_HOVER_SCALE, - DESKTOP_GRAPH_MAX_ZOOM, - DESKTOP_GRAPH_MIN_ZOOM, passedDesktopGraphDragThreshold, pickDesktopGraphNode, screenToDesktopGraph, + zoomDesktopGraphBy, zoomDesktopGraphAtPointer, } from './desktopGraphInteraction'; import { @@ -64,6 +63,11 @@ interface DesktopGraphNodeVisuals { selected: DesktopGraphNodeVisual; } +interface DesktopGraphViewportControls { + fitToScreen(): void; + zoomBy(factor: number): void; +} + const GRAPH_APPEARANCE_DEFAULTS: DesktopGraphAppearance = { labelForeground: 'CanvasText', labelMutedForeground: 'GrayText', @@ -183,24 +187,28 @@ export function useDesktopGraphRenderer({ graph, physicsSettings, selectedId, - onSelect, + onSelectionChange, }: { graph: DesktopGraph; physicsSettings: GraphPhysicsSettings; selectedId?: string; - onSelect: (id: string) => void; + onSelectionChange: (id: string | undefined) => void; }): { canvasRef: React.RefObject; + fitToScreen: () => void; overlayRef: React.RefObject; rendererError: string | undefined; + zoomIn: () => void; + zoomOut: () => void; } { const canvasRef = useRef(null); const overlayRef = useRef(null); - const onSelectRef = useRef(onSelect); + const onSelectionChangeRef = useRef(onSelectionChange); const physicsSettingsRef = useRef(physicsSettings); const selectedIdRef = useRef(selectedId); const applyPhysicsSettingsRef = useRef<((settings: GraphPhysicsSettings) => void) | undefined>(); const redrawRef = useRef<(() => void) | undefined>(undefined); + const viewportControlsRef = useRef(); const [materialIcons, setMaterialIcons] = useState(); const [physicsReady, setPhysicsReady] = useState(false); const [rendererError, setRendererError] = useState(); @@ -211,8 +219,18 @@ export function useDesktopGraphRenderer({ }, [selectedId]); useEffect(() => { - onSelectRef.current = onSelect; - }, [onSelect]); + onSelectionChangeRef.current = onSelectionChange; + }, [onSelectionChange]); + + const fitToScreen = useCallback((): void => { + viewportControlsRef.current?.fitToScreen(); + }, []); + const zoomIn = useCallback((): void => { + viewportControlsRef.current?.zoomBy(1.2); + }, []); + const zoomOut = useCallback((): void => { + viewportControlsRef.current?.zoomBy(1 / 1.2); + }, []); useEffect(() => { physicsSettingsRef.current = physicsSettings; @@ -486,7 +504,18 @@ export function useDesktopGraphRenderer({ styleVersion += 1; scheduleVisible(); }; + const zoomBy = (factor: number): void => { + fitFrames = 0; + camera = zoomDesktopGraphBy(camera, factor); + scheduleVisible(); + }; + const fitToGraph = (): void => { + fitFrames = 0; + camera = fitCamera(layout.x, layout.y, canvas.clientWidth, canvas.clientHeight); + scheduleVisible(); + }; redrawRef.current = restyle; + viewportControlsRef.current = { fitToScreen: fitToGraph, zoomBy }; applyPhysicsSettingsRef.current = (settings): void => { applyGraphPhysicsSettings(layout, settings); continueAfterFrame = true; @@ -576,14 +605,16 @@ export function useDesktopGraphRenderer({ return; } - const deltaX = event.clientX - pointer.x; - const deltaY = event.clientY - pointer.y; - pointer.moved ||= passedDesktopGraphDragThreshold( + const passedThreshold = pointer.moved || passedDesktopGraphDragThreshold( pointer.originX, pointer.originY, event.clientX, event.clientY, ); + if (!passedThreshold) return; + pointer.moved = true; + const deltaX = event.clientX - pointer.x; + const deltaY = event.clientY - pointer.y; camera.centerX -= deltaX / camera.zoom; camera.centerY -= deltaY / camera.zoom; pointer.x = event.clientX; @@ -594,14 +625,19 @@ export function useDesktopGraphRenderer({ const currentPointer = pointer; pointer = undefined; if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId); - if (!currentPointer || currentPointer.nodeIndex === undefined) return; + if (!currentPointer) return; + if (currentPointer.nodeIndex === undefined) { + if (!currentPointer.moved) onSelectionChangeRef.current(undefined); + scheduleVisible(); + return; + } const source = graph.nodes[currentPointer.nodeIndex]; if (currentPointer.moved) { layout.release(currentPointer.nodeIndex); layout.setAlphaTarget(0); continueAfterFrame = true; - } else if (source?.nodeType !== 'folder' && source) { - onSelectRef.current(source.id); + } else if (source) { + onSelectionChangeRef.current(source.id); } scheduleVisible(); }; @@ -628,19 +664,16 @@ export function useDesktopGraphRenderer({ else if (event.key === 'ArrowUp') camera.centerY -= panDistance; else if (event.key === 'ArrowDown') camera.centerY += panDistance; else if (event.key === '+' || event.key === '=') { - camera.zoom = Math.min(DESKTOP_GRAPH_MAX_ZOOM, camera.zoom * 1.2); + zoomBy(1.2); } else if (event.key === '-' || event.key === '_') { - camera.zoom = Math.max(DESKTOP_GRAPH_MIN_ZOOM, camera.zoom / 1.2); - } else if (event.key === '0') camera = fitCamera( - layout.x, - layout.y, - canvas.clientWidth, - canvas.clientHeight, - ); + zoomBy(1 / 1.2); + } else if (event.key === '0') fitToGraph(); else return; event.preventDefault(); - fitFrames = 0; - scheduleVisible(); + if (event.key.startsWith('Arrow')) { + fitFrames = 0; + scheduleVisible(); + } }; canvas.addEventListener('wheel', onWheel, { passive: false }); canvas.addEventListener('pointerdown', onPointerDown); @@ -672,6 +705,7 @@ export function useDesktopGraphRenderer({ return () => { active = false; redrawRef.current = undefined; + viewportControlsRef.current = undefined; applyPhysicsSettingsRef.current = undefined; if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); resize.disconnect(); @@ -687,5 +721,5 @@ export function useDesktopGraphRenderer({ }; }, [graph, materialIcons, physicsReady]); - return { canvasRef, overlayRef, rendererError }; + return { canvasRef, fitToScreen, overlayRef, rendererError, zoomIn, zoomOut }; } diff --git a/apps/desktop/src/useDesktopWorkspace.test.tsx b/apps/desktop/src/useDesktopWorkspace.test.tsx new file mode 100644 index 000000000..d4987bc67 --- /dev/null +++ b/apps/desktop/src/useDesktopWorkspace.test.tsx @@ -0,0 +1,98 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const bridge = vi.hoisted(() => ({ + chooseWorkspace: vi.fn(), + clearRecentWorkspaces: vi.fn(async () => undefined), + closeWorkspace: vi.fn(async () => undefined), + initialWorkspace: vi.fn(async () => undefined), + listRecentWorkspaces: vi.fn(async () => []), + listenToDesktopMenu: vi.fn(async () => () => undefined), + loadWorkspaceGraph: vi.fn(), + readDesktopGraphSettings: vi.fn(), + readWorkspaceFile: vi.fn(), + saveWorkspaceFile: vi.fn(), + writeDesktopGraphSettings: vi.fn(), +})); + +vi.mock('./bridge', () => bridge); + +import { useDesktopWorkspace } from './useDesktopWorkspace'; + +let currentWorkspace: ReturnType | undefined; + +function current(): ReturnType { + if (!currentWorkspace) throw new Error('Desktop workspace hook is not mounted.'); + return currentWorkspace; +} + +function Harness(): null { + currentWorkspace = useDesktopWorkspace(); + return null; +} + +describe('desktop document and graph selection ownership', () => { + beforeEach(() => { + currentWorkspace = undefined; + vi.clearAllMocks(); + bridge.initialWorkspace.mockResolvedValue(undefined); + bridge.listRecentWorkspaces.mockResolvedValue([]); + bridge.listenToDesktopMenu.mockResolvedValue(() => undefined); + bridge.readWorkspaceFile.mockImplementation(async (path: string) => ({ + path, + content: `export const path = ${JSON.stringify(path)};`, + revision: 'revision-1', + })); + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + callback(0); + return 1; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('clears graph focus and closes a File independently', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render()); + await act(async () => current().selectFile('src/main.ts')); + expect(current().document?.path).toBe('src/main.ts'); + expect(current().selectedGraphNodeId).toBe('src/main.ts'); + + await act(async () => current().clearGraphSelection()); + expect(current().selectedGraphNodeId).toBeUndefined(); + expect(current().document?.path).toBe('src/main.ts'); + + await act(async () => current().selectGraphNode('src/main.ts')); + await act(async () => current().closeCurrentDocument()); + expect(current().document).toBeUndefined(); + expect(current().selectedGraphNodeId).toBe('src/main.ts'); + await act(async () => root.unmount()); + }); + + it('uses the existing dirty confirmation before Close File discards a draft', async () => { + const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false); + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render()); + await act(async () => current().selectFile('src/main.ts')); + await act(async () => current().setDraft('unsaved')); + await act(async () => current().closeCurrentDocument()); + expect(current().document?.path).toBe('src/main.ts'); + expect(confirm).toHaveBeenCalledWith('Discard the unsaved edit and close this File?'); + + confirm.mockReturnValue(true); + await act(async () => current().closeCurrentDocument()); + expect(current().document).toBeUndefined(); + expect(current().dirty).toBe(false); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/useDesktopWorkspace.ts b/apps/desktop/src/useDesktopWorkspace.ts index 02088e5cf..7c2c3e700 100644 --- a/apps/desktop/src/useDesktopWorkspace.ts +++ b/apps/desktop/src/useDesktopWorkspace.ts @@ -1,5 +1,6 @@ import { DEFAULT_GRAPH_PHYSICS_SETTINGS, + normalizeGraphPhysicsSetting, type GraphPhysicsSettings, } from '@codegraphy-dev/graph-visuals'; import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; @@ -47,6 +48,7 @@ interface DesktopWorkspaceState { pendingWorkspaceAction?: WorkspaceAction; recentWorkspaces: RecentWorkspace[]; saving: boolean; + selectedGraphNodeId?: string; status: AppStatus; workspaceRoot?: string; } @@ -54,10 +56,12 @@ interface DesktopWorkspaceState { type DesktopWorkspaceEvent = | { type: 'close_workspace' } | { type: 'file_metrics'; elapsed: number; path: string } + | { type: 'file_closed' } | { type: 'file_opened'; document: FileDocument } | { type: 'file_pending'; path?: string } | { type: 'file_saved'; document: FileDocument } | { type: 'graph_settings'; settings: GraphPhysicsSettings } + | { type: 'graph_selected'; id?: string } | { type: 'recent_workspaces'; workspaces: RecentWorkspace[] } | { type: 'saving'; saving: boolean } | { type: 'set_draft'; draft: string } @@ -93,6 +97,13 @@ function desktopWorkspaceReducer( samples: [...(state.fileSwitchMetrics?.samples ?? []), event.elapsed].slice(-24), }, }; + case 'file_closed': + return { + ...state, + document: undefined, + draft: '', + pendingFilePath: undefined, + }; case 'file_opened': return { ...state, @@ -106,6 +117,8 @@ function desktopWorkspaceReducer( return { ...state, document: event.document, draft: event.document.content }; case 'graph_settings': return { ...state, graphSettings: event.settings }; + case 'graph_selected': + return { ...state, selectedGraphNodeId: event.id }; case 'recent_workspaces': return { ...state, recentWorkspaces: event.workspaces }; case 'saving': @@ -122,6 +135,12 @@ function desktopWorkspaceReducer( graphRevision: state.graphRevision + 1, workspaceRoot: event.result.workspaceRoot, graphResult: event.result, + selectedGraphNodeId: !event.changed + && event.result.kind === 'ready' + && state.selectedGraphNodeId + && event.result.graph.nodes.some(node => node.id === state.selectedGraphNodeId) + ? state.selectedGraphNodeId + : undefined, ...(event.changed ? { document: undefined, @@ -253,7 +272,10 @@ export function useDesktopWorkspace() { key: keyof GraphPhysicsSettings, value: number, ): void => { - const settings = { ...graphSettingsRef.current, [key]: value }; + const settings = { + ...graphSettingsRef.current, + [key]: normalizeGraphPhysicsSetting(key, value), + }; graphSettingsRef.current = settings; graphSettingsDirtyRef.current = true; dispatch({ type: 'graph_settings', settings }); @@ -288,6 +310,7 @@ export function useDesktopWorkspace() { const document = await readWorkspaceFile(path); if (requestId !== fileRequestRef.current) return; dispatch({ type: 'file_opened', document }); + dispatch({ type: 'graph_selected', id: path }); requestAnimationFrame(() => requestAnimationFrame(() => { if (requestId !== fileRequestRef.current) return; dispatch({ type: 'file_metrics', elapsed: performance.now() - startedAt, path }); @@ -299,6 +322,20 @@ export function useDesktopWorkspace() { } }, [dirty, reportError, state.document?.path, state.pendingFilePath]); + const selectGraphNode = useCallback((id: string | undefined): void => { + dispatch({ type: 'graph_selected', id }); + if (!id) return; + const node = graph?.nodes.find(candidate => candidate.id === id); + if (node && node.nodeType !== 'folder') void selectFile(id); + }, [graph?.nodes, selectFile]); + + const closeCurrentDocument = useCallback((): void => { + if ((!state.document && !state.pendingFilePath) || state.saving) return; + if (dirty && !window.confirm('Discard the unsaved edit and close this File?')) return; + fileRequestRef.current += 1; + dispatch({ type: 'file_closed' }); + }, [dirty, state.document, state.pendingFilePath, state.saving]); + const saveCurrentDocument = useCallback(async (): Promise => { if (!state.document || !dirty || state.saving || !workspaceRootRef.current) return !dirty; const root = workspaceRootRef.current; @@ -362,17 +399,20 @@ export function useDesktopWorkspace() { }, [dirty, executeWorkspaceAction]); const requestWorkspaceActionRef = useRef(requestWorkspaceAction); + const closeCurrentDocumentRef = useRef(closeCurrentDocument); const saveCurrentDocumentRef = useRef(saveCurrentDocument); useEffect(() => { requestWorkspaceActionRef.current = requestWorkspaceAction; + closeCurrentDocumentRef.current = closeCurrentDocument; saveCurrentDocumentRef.current = saveCurrentDocument; - }, [requestWorkspaceAction, saveCurrentDocument]); + }, [closeCurrentDocument, requestWorkspaceAction, saveCurrentDocument]); useEffect(() => { let active = true; let stop: (() => void) | undefined; void listenToDesktopMenu({ + closeFile: () => closeCurrentDocumentRef.current(), closeWorkspace: () => requestWorkspaceActionRef.current({ kind: 'close' }), openRecent: path => requestWorkspaceActionRef.current({ kind: 'open', path }), openWorkspace: () => requestWorkspaceActionRef.current({ kind: 'choose' }), @@ -419,6 +459,8 @@ export function useDesktopWorkspace() { tree, cancelPendingWorkspaceAction: () => dispatch({ type: 'workspace_action' }), clearRecent, + clearGraphSelection: () => dispatch({ type: 'graph_selected' }), + closeCurrentDocument, finishPendingWorkspaceAction, flushGraphSettings, openRecentWorkspace: (path: string) => requestWorkspaceAction({ kind: 'open', path }), @@ -427,6 +469,7 @@ export function useDesktopWorkspace() { resetGraphSettings, saveCurrentDocument, selectFile, + selectGraphNode, setDraft: (draft: string) => dispatch({ type: 'set_draft', draft }), updateGraphSetting, }; diff --git a/docs/RELEASING.md b/docs/RELEASING.md index bd4c58200..a0c17bbd3 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -80,8 +80,11 @@ Run this check on the draft asset before you publish it: 7. Open a real CodeGraphy Workspace. Confirm the File and Folder hierarchy appears. 8. Open a source File, edit it, save it, and confirm Core reports one-File incremental Indexing. 9. Confirm the right pane draws the Core-owned File and Folder Relationship Graph with WebGPU. -10. Change the open File outside CodeGraphy, then confirm the app rejects an overwrite until the File is reopened. -11. Close the app and confirm its `codegraphy-core` child process exits. +10. Filter the File hierarchy, switch Files with only the keyboard, resize both pane separators, and confirm focus stays in the hierarchy. +11. Select a Node, click empty graph background, and confirm the editor File stays open. Verify Zoom In, Zoom Out, and Fit to Screen. +12. Close the editor File and confirm the workspace and Relationship Graph stay open. Repeat with a dirty File and confirm the discard prompt appears. +13. Change the open File outside CodeGraphy, then confirm the app rejects an overwrite until the File is reopened. +14. Close the app and confirm its `codegraphy-core` child process exits. Keep the release as a draft if any step fails. After all steps pass, publish the GitHub Release, update the website to link the exact public DMG, and verify that link from a clean browser session. The website must not point at a draft, a workflow artifact, or an ad-hoc build. diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index d697a0b48..021cf9807 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -100,6 +100,8 @@ Edge colors come from Edge Type definitions and Legend layers. There is no curre ``` The Settings > Forces controls apply these values to the live WebAssembly layout. +Link Distance accepts 30 through 150 in both the Extension and desktop Graph View. +The shared settings boundary clamps older finite values into that range before use or persistence. The macOS desktop app uses the same defaults, ranges, and force mapping. Its first Graph Settings surface exposes Repel Force, Center Force, and Link Distance and diff --git a/packages/extension/src/e2e/suite/settings.test.ts b/packages/extension/src/e2e/suite/settings.test.ts index 67bf8d330..f0dc5fa43 100644 --- a/packages/extension/src/e2e/suite/settings.test.ts +++ b/packages/extension/src/e2e/suite/settings.test.ts @@ -75,7 +75,7 @@ suite('Settings: Physics', function () { // Should not throw await api.dispatchWebviewMessage({ type: 'UPDATE_PHYSICS_SETTING', - payload: { key: 'linkDistance', value: 200 }, + payload: { key: 'linkDistance', value: 150 }, }); await sleep(500); diff --git a/packages/extension/src/extension/graphView/settings/physics/reader.ts b/packages/extension/src/extension/graphView/settings/physics/reader.ts index b6bd20d1c..257df5acd 100644 --- a/packages/extension/src/extension/graphView/settings/physics/reader.ts +++ b/packages/extension/src/extension/graphView/settings/physics/reader.ts @@ -1,15 +1,18 @@ import type * as vscode from 'vscode'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import { + normalizeGraphPhysicsSettings, + type GraphPhysicsSettings, +} from '@codegraphy-dev/graph-visuals'; export function readGraphViewPhysicsSettings( config: Pick, defaults: GraphPhysicsSettings ): GraphPhysicsSettings { - return { + return normalizeGraphPhysicsSettings({ repelForce: config.get('physics.repelForce', defaults.repelForce), linkDistance: config.get('physics.linkDistance', defaults.linkDistance), linkForce: config.get('physics.linkForce', defaults.linkForce), damping: config.get('physics.damping', defaults.damping), centerForce: config.get('physics.centerForce', defaults.centerForce), - }; + }); } diff --git a/packages/extension/src/extension/graphView/settings/physics/updates.ts b/packages/extension/src/extension/graphView/settings/physics/updates.ts index 681e73f82..ed083f379 100644 --- a/packages/extension/src/extension/graphView/settings/physics/updates.ts +++ b/packages/extension/src/extension/graphView/settings/physics/updates.ts @@ -1,4 +1,7 @@ -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import { + normalizeGraphPhysicsSetting, + type GraphPhysicsSettings, +} from '@codegraphy-dev/graph-visuals'; interface GraphViewPhysicsConfigurationLike { update(key: string, value: unknown, target?: unknown): PromiseLike; @@ -32,7 +35,12 @@ export async function updateGraphViewPhysicsSetting( value: number, { getConfiguration, getConfigTarget }: GraphViewPhysicsConfigOptions, ): Promise { - await updatePhysicsSetting(getConfiguration(), key, value, getConfigTarget?.()); + await updatePhysicsSetting( + getConfiguration(), + key, + normalizeGraphPhysicsSetting(key, value), + getConfigTarget?.(), + ); } export async function resetGraphViewPhysicsSettings( diff --git a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts index ea4eca09e..c02a3feef 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts @@ -20,8 +20,8 @@ describe('graphView/settings/physics/reader', () => { repelForce: 11, linkDistance: 81, linkForce: 1.15, - damping: 1.7, - centerForce: 1.1, + damping: 1, + centerForce: 1, }); expect(get.mock.calls.map(([key]) => key)).toEqual([ 'physics.repelForce', @@ -31,4 +31,12 @@ describe('graphView/settings/physics/reader', () => { 'physics.centerForce', ]); }); + + it('clamps a stored Link Distance through the shared settings boundary', () => { + const config = { + get: vi.fn((key: string, fallback: number) => key === 'physics.linkDistance' ? 500 : fallback), + } as unknown as vscode.WorkspaceConfiguration; + + expect(readGraphViewPhysicsSettings(config, defaults).linkDistance).toBe(150); + }); }); diff --git a/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts b/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts index 2c4a986e1..b6435db5e 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts @@ -12,7 +12,17 @@ describe('graph view physics update helpers', () => { getConfiguration: () => ({ update }), }); - expect(update).toHaveBeenCalledWith('physics.repelForce', 25); + expect(update).toHaveBeenCalledWith('physics.repelForce', 20); + }); + + it('clamps Link Distance before persistence', async () => { + const update = vi.fn(() => Promise.resolve()); + + await updateGraphViewPhysicsSetting('linkDistance', 500, { + getConfiguration: () => ({ update }), + }); + + expect(update).toHaveBeenCalledWith('physics.linkDistance', 150); }); it('updates a single physics setting at the requested configuration target', async () => { diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts index 35658cb2b..bb06300e5 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts @@ -61,13 +61,13 @@ describe('owned graph layout settings', () => { expect(toGraphPhysicsLayoutConfig({ centerForce: 1, damping: 0.4, - linkDistance: 500, + linkDistance: 150, linkForce: 1, repelForce: 20, })).toEqual({ centralGravity: 1, chargeStrength: -500, - linkDistance: 500, + linkDistance: 150, linkStrength: 1, velocityDecay: 0.4, }); @@ -93,7 +93,7 @@ describe('owned graph layout settings', () => { run(short); const long = twoNodeEngine(300); - long.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 500, linkForce: 1 })); + long.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 150, linkForce: 1 })); run(long); const disabled = twoNodeEngine(300); diff --git a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx index e585c2c66..6c6bd6cd2 100644 --- a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx @@ -83,7 +83,7 @@ describe('ForcesSection', () => { expect(screen.getByTestId('center-force-slider-change')).toHaveAttribute('data-maximum', '1'); expect(screen.getByTestId('center-force-slider-change')).toHaveAttribute('data-step', '0.01'); expect(screen.getByTestId('link-distance-slider-change')).toHaveAttribute('data-minimum', '30'); - expect(screen.getByTestId('link-distance-slider-change')).toHaveAttribute('data-maximum', '500'); + expect(screen.getByTestId('link-distance-slider-change')).toHaveAttribute('data-maximum', '150'); expect(screen.getByTestId('link-distance-slider-change')).toHaveAttribute('data-step', '10'); expect(screen.getByTestId('link-force-slider-change')).toHaveAttribute('data-maximum', '2'); expect(screen.getByTestId('link-force-slider-change')).toHaveAttribute('data-step', '0.01'); diff --git a/packages/graph-visuals/src/physics.ts b/packages/graph-visuals/src/physics.ts index 101501f44..64f96b162 100644 --- a/packages/graph-visuals/src/physics.ts +++ b/packages/graph-visuals/src/physics.ts @@ -17,7 +17,7 @@ export const DEFAULT_GRAPH_PHYSICS_SETTINGS: GraphPhysicsSettings = { export const GRAPH_PHYSICS_CONTROL_LIMITS = { repelForce: { min: 0, max: 20, step: 1 }, centerForce: { min: 0, max: 1, step: 0.01 }, - linkDistance: { min: 30, max: 500, step: 10 }, + linkDistance: { min: 30, max: 150, step: 10 }, linkForce: { min: 0, max: 2, step: 0.01 }, } as const; @@ -36,41 +36,39 @@ export interface GraphPhysicsConfigTarget { export function toGraphPhysicsLayoutConfig( settings: GraphPhysicsSettings, ): GraphLayoutPhysicsConfig { - const repelForce = normalizedSetting( - settings.repelForce, - GRAPH_PHYSICS_CONTROL_LIMITS.repelForce.min, - GRAPH_PHYSICS_CONTROL_LIMITS.repelForce.max, - DEFAULT_GRAPH_PHYSICS_SETTINGS.repelForce, - ); + const normalized = normalizeGraphPhysicsSettings(settings); return { - centralGravity: normalizedSetting( - settings.centerForce, - GRAPH_PHYSICS_CONTROL_LIMITS.centerForce.min, - GRAPH_PHYSICS_CONTROL_LIMITS.centerForce.max, - DEFAULT_GRAPH_PHYSICS_SETTINGS.centerForce, - ), - chargeStrength: -(repelForce / GRAPH_PHYSICS_CONTROL_LIMITS.repelForce.max) * 500, - linkDistance: normalizedSetting( - settings.linkDistance, - GRAPH_PHYSICS_CONTROL_LIMITS.linkDistance.min, - GRAPH_PHYSICS_CONTROL_LIMITS.linkDistance.max, - DEFAULT_GRAPH_PHYSICS_SETTINGS.linkDistance, - ), - linkStrength: normalizedSetting( - settings.linkForce, - GRAPH_PHYSICS_CONTROL_LIMITS.linkForce.min, - GRAPH_PHYSICS_CONTROL_LIMITS.linkForce.max, - DEFAULT_GRAPH_PHYSICS_SETTINGS.linkForce, - ), - velocityDecay: normalizedSetting( - settings.damping, - 0, - 1, - DEFAULT_GRAPH_PHYSICS_SETTINGS.damping, - ), + centralGravity: normalized.centerForce, + chargeStrength: -(normalized.repelForce / GRAPH_PHYSICS_CONTROL_LIMITS.repelForce.max) * 500, + linkDistance: normalized.linkDistance, + linkStrength: normalized.linkForce, + velocityDecay: normalized.damping, }; } +export function normalizeGraphPhysicsSettings( + settings: GraphPhysicsSettings, +): GraphPhysicsSettings { + return { + repelForce: normalizeGraphPhysicsSetting('repelForce', settings.repelForce), + centerForce: normalizeGraphPhysicsSetting('centerForce', settings.centerForce), + linkDistance: normalizeGraphPhysicsSetting('linkDistance', settings.linkDistance), + linkForce: normalizeGraphPhysicsSetting('linkForce', settings.linkForce), + damping: normalizeGraphPhysicsSetting('damping', settings.damping), + }; +} + +export function normalizeGraphPhysicsSetting( + key: keyof GraphPhysicsSettings, + value: number, +): number { + if (key === 'damping') { + return normalizedSetting(value, 0, 1, DEFAULT_GRAPH_PHYSICS_SETTINGS.damping); + } + const limits = GRAPH_PHYSICS_CONTROL_LIMITS[key]; + return normalizedSetting(value, limits.min, limits.max, DEFAULT_GRAPH_PHYSICS_SETTINGS[key]); +} + export function applyGraphPhysicsSettings( target: GraphPhysicsConfigTarget, settings: GraphPhysicsSettings, diff --git a/packages/graph-visuals/tests/physics.test.ts b/packages/graph-visuals/tests/physics.test.ts index 2efe7b4c2..1bd8f66c7 100644 --- a/packages/graph-visuals/tests/physics.test.ts +++ b/packages/graph-visuals/tests/physics.test.ts @@ -3,6 +3,8 @@ import { DEFAULT_GRAPH_PHYSICS_SETTINGS, GRAPH_PHYSICS_CONTROL_LIMITS, applyGraphPhysicsSettings, + normalizeGraphPhysicsSetting, + normalizeGraphPhysicsSettings, toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, } from '../src/index.js'; @@ -19,7 +21,7 @@ describe('graph physics contract', () => { expect(GRAPH_PHYSICS_CONTROL_LIMITS).toEqual({ repelForce: { min: 0, max: 20, step: 1 }, centerForce: { min: 0, max: 1, step: 0.01 }, - linkDistance: { min: 30, max: 500, step: 10 }, + linkDistance: { min: 30, max: 150, step: 10 }, linkForce: { min: 0, max: 2, step: 0.01 }, }); }); @@ -52,6 +54,23 @@ describe('graph physics contract', () => { }); }); + it('normalizes persisted settings through the shared visible ranges', () => { + expect(normalizeGraphPhysicsSettings({ + repelForce: -4, + centerForce: 2, + linkDistance: 500, + linkForce: 3, + damping: -1, + })).toEqual({ + repelForce: 0, + centerForce: 1, + linkDistance: 150, + linkForce: 2, + damping: 0, + }); + expect(normalizeGraphPhysicsSetting('linkDistance', 500)).toBe(150); + }); + it('applies the mapped config through the renderer boundary', () => { const setConfig = vi.fn(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0af6c14a..d5247cc52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: '@codemirror/state': specifier: ^6.7.1 version: 6.7.1 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 '@codemirror/view': specifier: ^6.43.8 version: 6.43.8 @@ -1147,6 +1150,9 @@ packages: '@codemirror/state@6.7.1': resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + '@codemirror/view@6.43.8': resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} @@ -8577,6 +8583,13 @@ snapshots: dependencies: '@marijn/find-cluster-break': 1.0.3 + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/highlight': 1.2.3 + '@codemirror/view@6.43.8': dependencies: '@codemirror/state': 6.7.1 From cd40a17e43a2a51f5048a892c21ccb2a348404fd Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 18:21:35 -0700 Subject: [PATCH 15/25] fix(desktop): own hierarchy search dismissal --- apps/desktop/src/components/FileTree.test.tsx | 8 ++++-- apps/desktop/src/components/FileTree.tsx | 26 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/components/FileTree.test.tsx b/apps/desktop/src/components/FileTree.test.tsx index 0d11c245c..36ba61f12 100644 --- a/apps/desktop/src/components/FileTree.test.tsx +++ b/apps/desktop/src/components/FileTree.test.tsx @@ -26,6 +26,10 @@ function key(target: Element, value: string, options: KeyboardEventInit = {}): v target.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: value, ...options })); } +function keyUp(target: Element, value: string): void { + target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value })); +} + function setInputValue(input: HTMLInputElement, value: string): void { const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); if (!descriptor?.set) throw new Error('The input value setter is unavailable.'); @@ -94,7 +98,7 @@ describe('File hierarchy keyboard and filter behavior', () => { const main = host.querySelector('[data-tree-path="src/main.ts"]'); await act(async () => main?.focus()); await act(async () => { if (main) key(main, 'f', { metaKey: true }); }); - const filter = host.querySelector('input[type="search"]'); + const filter = host.querySelector('[role="searchbox"]'); expect(document.activeElement).toBe(filter); await act(async () => { @@ -109,7 +113,7 @@ describe('File hierarchy keyboard and filter behavior', () => { setInputValue(filter, 'not-present'); }); expect(host.textContent).toContain('No Files or Folders match'); - await act(async () => { if (filter) key(filter, 'Escape'); }); + await act(async () => { if (filter) keyUp(filter, 'Escape'); }); await act(async () => animationFrames.splice(0).forEach(callback => callback(0))); expect(host.querySelectorAll('[role="treeitem"]')).toHaveLength(4); expect(document.activeElement).toBe( diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx index e7875cdf9..1958fbb9d 100644 --- a/apps/desktop/src/components/FileTree.tsx +++ b/apps/desktop/src/components/FileTree.tsx @@ -183,6 +183,11 @@ export function FileTree({ requestAnimationFrame(() => focusPath(activePath)); }; + const clearFilterAndRestoreFocus = (): void => { + setFilter(''); + focusPath(restorePath); + }; + const handleTreeKeyDown = (event: React.KeyboardEvent): void => { const tree = treeRef.current; if (!tree || !(event.target instanceof HTMLButtonElement)) return; @@ -251,26 +256,25 @@ export function FileTree({ setFilter(event.currentTarget.value)} - onKeyDown={(event) => { + onKeyDownCapture={(event) => { if (event.key === 'ArrowDown') { event.preventDefault(); focusTreeFromFilter(); } else if (event.key === 'Escape') { event.preventDefault(); - setFilter(''); - requestAnimationFrame(() => { - if (!restorePath) return; - setFocusedPath(restorePath); - const target = treeItems(browserRef.current ?? document.body) - .find(item => item.dataset.treePath === restorePath); - target?.focus(); - target?.scrollIntoView?.({ block: 'nearest' }); - }); + clearFilterAndRestoreFocus(); } }} + onKeyUp={(event) => { + if (event.key !== 'Escape') return; + event.preventDefault(); + clearFilterAndRestoreFocus(); + }} + inputMode="search" placeholder="Filter Files and Folders" ref={filterRef} - type="search" + role="searchbox" + type="text" value={filter} /> ⌘F From dd011cb42fd311e88f4bbb7f23a0c7fb76f32ba6 Mon Sep 17 00:00:00 2001 From: joesobo Date: Thu, 6 Aug 2026 18:34:30 -0700 Subject: [PATCH 16/25] fix(desktop): confirm unsaved File actions --- apps/desktop/src/App.tsx | 16 +++- .../src/components/UnsavedFileDialog.tsx | 77 +++++++++++++++++++ .../desktop/src/components/WorkspacePanes.tsx | 13 +++- .../src/components/WorkspaceSwitchDialog.tsx | 50 ------------ apps/desktop/src/useDesktopWorkspace.test.tsx | 11 ++- apps/desktop/src/useDesktopWorkspace.ts | 51 ++++++++++-- 6 files changed, 150 insertions(+), 68 deletions(-) create mode 100644 apps/desktop/src/components/UnsavedFileDialog.tsx delete mode 100644 apps/desktop/src/components/WorkspaceSwitchDialog.tsx diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 96aa78ad8..76879ba4b 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -3,8 +3,8 @@ import { CodeEditor } from './components/CodeEditor'; import { FileTree } from './components/FileTree'; import { GraphPanel } from './components/GraphPanel'; import { GraphSettingsPopover } from './components/GraphSettingsPopover'; +import { UnsavedFileDialog } from './components/UnsavedFileDialog'; import { WorkspacePanes } from './components/WorkspacePanes'; -import { WorkspaceSwitchDialog } from './components/WorkspaceSwitchDialog'; import { WorkspaceSwitcher } from './components/WorkspaceSwitcher'; import { formatGraphCounts } from './model'; import { useDesktopWorkspace } from './useDesktopWorkspace'; @@ -135,9 +135,21 @@ export function App(): React.ReactElement { Local only · macOS 26+ + {workspace.pendingFileAction && workspace.document ? ( + void workspace.finishPendingFileAction(false)} + onSave={() => void workspace.finishPendingFileAction(true)} + saving={workspace.saving} + /> + ) : null} + {workspace.pendingWorkspaceAction && workspace.document ? ( - void workspace.finishPendingWorkspaceAction(false)} onSave={() => void workspace.finishPendingWorkspaceAction(true)} diff --git a/apps/desktop/src/components/UnsavedFileDialog.tsx b/apps/desktop/src/components/UnsavedFileDialog.tsx new file mode 100644 index 000000000..eef5f0818 --- /dev/null +++ b/apps/desktop/src/components/UnsavedFileDialog.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef } from 'react'; + +type UnsavedFileIntent = 'close-file' | 'open-file' | 'switch-workspace'; + +const COPY: Record = { + 'close-file': { + description: 'Closing clears only the editor. The workspace and Relationship Graph stay open.', + primary: 'Save and Close', + title: 'Save this File before closing?', + }, + 'open-file': { + description: 'Cancel keeps the current File open.', + primary: 'Save and Open', + title: 'Save this File before opening another?', + }, + 'switch-workspace': { + description: 'Cancel keeps the current workspace open.', + primary: 'Save and Switch', + title: 'Save this File before switching?', + }, +}; + +export function UnsavedFileDialog({ + filePath, + intent, + onCancel, + onDiscard, + onSave, + saving, +}: { + filePath: string; + intent: UnsavedFileIntent; + onCancel: () => void; + onDiscard: () => void; + onSave: () => void; + saving: boolean; +}): React.ReactElement { + const dialogRef = useRef(null); + const copy = COPY[intent]; + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + dialog.showModal(); + return () => dialog.close(); + }, []); + + return ( + { + event.preventDefault(); + onCancel(); + }} + ref={dialogRef} + > + +

{copy.title}

+

+ {filePath} has changes that are not saved. {copy.description} +

+
+ + + +
+
+ ); +} diff --git a/apps/desktop/src/components/WorkspacePanes.tsx b/apps/desktop/src/components/WorkspacePanes.tsx index 79b26be1a..a1eb527d3 100644 --- a/apps/desktop/src/components/WorkspacePanes.tsx +++ b/apps/desktop/src/components/WorkspacePanes.tsx @@ -46,7 +46,6 @@ export function WorkspacePanes({ const containerRef = useRef(null); const dragRef = useRef(); const layoutRef = useRef(layout); - layoutRef.current = layout; const pixels = clampPaneLayout(layout, containerWidth); useEffect(() => { @@ -61,13 +60,17 @@ export function WorkspacePanes({ const updateFromPixels = (next: Pick): void => { const normalized = clampPaneLayout(paneLayoutFromPixels(next, containerWidth), containerWidth); - setLayout(paneLayoutFromPixels(normalized, containerWidth)); + const nextLayout = paneLayoutFromPixels(normalized, containerWidth); + layoutRef.current = nextLayout; + setLayout(nextLayout); }; const commitLayout = (): void => savePaneLayout(layoutRef.current); const resetLayout = (): void => { - setLayout({ ...DEFAULT_PANE_LAYOUT }); + const nextLayout = { ...DEFAULT_PANE_LAYOUT }; + layoutRef.current = nextLayout; + setLayout(nextLayout); savePaneLayout(DEFAULT_PANE_LAYOUT); }; @@ -156,7 +159,9 @@ export function WorkspacePanes({ if (!drag || drag.pointerId !== event.pointerId) return; dragRef.current = undefined; setActiveSeparator(undefined); - setLayout(paneLayoutFromPixels(drag.layout, containerWidth)); + const restoredLayout = paneLayoutFromPixels(drag.layout, containerWidth); + layoutRef.current = restoredLayout; + setLayout(restoredLayout); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } diff --git a/apps/desktop/src/components/WorkspaceSwitchDialog.tsx b/apps/desktop/src/components/WorkspaceSwitchDialog.tsx deleted file mode 100644 index 93e5b806d..000000000 --- a/apps/desktop/src/components/WorkspaceSwitchDialog.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export function WorkspaceSwitchDialog({ - filePath, - onCancel, - onDiscard, - onSave, - saving, -}: { - filePath: string; - onCancel: () => void; - onDiscard: () => void; - onSave: () => void; - saving: boolean; -}): React.ReactElement { - const dialogRef = useRef(null); - - useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - dialog.showModal(); - return () => dialog.close(); - }, []); - - return ( - { - event.preventDefault(); - onCancel(); - }} - ref={dialogRef} - > - -

Save this File before switching?

-

- {filePath} has changes that are not saved. Cancel keeps the current workspace open. -

-
- - - -
-
- ); -} diff --git a/apps/desktop/src/useDesktopWorkspace.test.tsx b/apps/desktop/src/useDesktopWorkspace.test.tsx index d4987bc67..216b9835a 100644 --- a/apps/desktop/src/useDesktopWorkspace.test.tsx +++ b/apps/desktop/src/useDesktopWorkspace.test.tsx @@ -77,8 +77,7 @@ describe('desktop document and graph selection ownership', () => { await act(async () => root.unmount()); }); - it('uses the existing dirty confirmation before Close File discards a draft', async () => { - const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false); + it('keeps dirty File actions pending until the user cancels or discards', async () => { const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); @@ -87,10 +86,14 @@ describe('desktop document and graph selection ownership', () => { await act(async () => current().setDraft('unsaved')); await act(async () => current().closeCurrentDocument()); expect(current().document?.path).toBe('src/main.ts'); - expect(confirm).toHaveBeenCalledWith('Discard the unsaved edit and close this File?'); + expect(current().pendingFileAction).toEqual({ kind: 'close' }); + + await act(async () => current().cancelPendingFileAction()); + expect(current().pendingFileAction).toBeUndefined(); + expect(current().document?.path).toBe('src/main.ts'); - confirm.mockReturnValue(true); await act(async () => current().closeCurrentDocument()); + await act(async () => current().finishPendingFileAction(false)); expect(current().document).toBeUndefined(); expect(current().dirty).toBe(false); await act(async () => root.unmount()); diff --git a/apps/desktop/src/useDesktopWorkspace.ts b/apps/desktop/src/useDesktopWorkspace.ts index 7c2c3e700..0b619643a 100644 --- a/apps/desktop/src/useDesktopWorkspace.ts +++ b/apps/desktop/src/useDesktopWorkspace.ts @@ -32,6 +32,10 @@ type WorkspaceAction = | { kind: 'close' } | { kind: 'open'; path: string }; +type FileAction = + | { kind: 'close' } + | { kind: 'open'; path: string }; + interface FileSwitchMetrics { lastPath: string; samples: number[]; @@ -44,6 +48,7 @@ interface DesktopWorkspaceState { graphRevision: number; graphResult?: WorkspaceGraphResult; graphSettings: GraphPhysicsSettings; + pendingFileAction?: FileAction; pendingFilePath?: string; pendingWorkspaceAction?: WorkspaceAction; recentWorkspaces: RecentWorkspace[]; @@ -58,6 +63,7 @@ type DesktopWorkspaceEvent = | { type: 'file_metrics'; elapsed: number; path: string } | { type: 'file_closed' } | { type: 'file_opened'; document: FileDocument } + | { type: 'file_action'; action?: FileAction } | { type: 'file_pending'; path?: string } | { type: 'file_saved'; document: FileDocument } | { type: 'graph_settings'; settings: GraphPhysicsSettings } @@ -102,6 +108,7 @@ function desktopWorkspaceReducer( ...state, document: undefined, draft: '', + pendingFileAction: undefined, pendingFilePath: undefined, }; case 'file_opened': @@ -111,6 +118,8 @@ function desktopWorkspaceReducer( draft: event.document.content, pendingFilePath: undefined, }; + case 'file_action': + return { ...state, pendingFileAction: event.action }; case 'file_pending': return { ...state, pendingFilePath: event.path }; case 'file_saved': @@ -147,6 +156,7 @@ function desktopWorkspaceReducer( draft: '', fileSwitchMetrics: undefined, graphSettings: DEFAULT_GRAPH_PHYSICS_SETTINGS, + pendingFileAction: undefined, pendingFilePath: undefined, } : {}), @@ -300,9 +310,7 @@ export function useDesktopWorkspace() { .catch(reportError); }, [openGraph, refreshRecentWorkspaces, reportError]); - const selectFile = useCallback(async (path: string): Promise => { - if (state.document?.path === path || state.pendingFilePath === path) return; - if (dirty && !window.confirm('Discard the unsaved edit and open another File?')) return; + const openFile = useCallback(async (path: string): Promise => { const requestId = ++fileRequestRef.current; const startedAt = performance.now(); dispatch({ type: 'file_pending', path }); @@ -320,7 +328,16 @@ export function useDesktopWorkspace() { dispatch({ type: 'file_pending' }); reportError(error); } - }, [dirty, reportError, state.document?.path, state.pendingFilePath]); + }, [reportError]); + + const selectFile = useCallback(async (path: string): Promise => { + if (state.document?.path === path || state.pendingFilePath === path) return; + if (dirty) { + dispatch({ type: 'file_action', action: { kind: 'open', path } }); + return; + } + await openFile(path); + }, [dirty, openFile, state.document?.path, state.pendingFilePath]); const selectGraphNode = useCallback((id: string | undefined): void => { dispatch({ type: 'graph_selected', id }); @@ -329,12 +346,19 @@ export function useDesktopWorkspace() { if (node && node.nodeType !== 'folder') void selectFile(id); }, [graph?.nodes, selectFile]); - const closeCurrentDocument = useCallback((): void => { - if ((!state.document && !state.pendingFilePath) || state.saving) return; - if (dirty && !window.confirm('Discard the unsaved edit and close this File?')) return; + const discardCurrentDocument = useCallback((): void => { fileRequestRef.current += 1; dispatch({ type: 'file_closed' }); - }, [dirty, state.document, state.pendingFilePath, state.saving]); + }, []); + + const closeCurrentDocument = useCallback((): void => { + if ((!state.document && !state.pendingFilePath) || state.saving) return; + if (dirty) { + dispatch({ type: 'file_action', action: { kind: 'close' } }); + return; + } + discardCurrentDocument(); + }, [dirty, discardCurrentDocument, state.document, state.pendingFilePath, state.saving]); const saveCurrentDocument = useCallback(async (): Promise => { if (!state.document || !dirty || state.saving || !workspaceRootRef.current) return !dirty; @@ -449,6 +473,15 @@ export function useDesktopWorkspace() { await executeWorkspaceAction(action); }, [executeWorkspaceAction, saveCurrentDocument, state.pendingWorkspaceAction]); + const finishPendingFileAction = useCallback(async (save: boolean): Promise => { + const action = state.pendingFileAction; + if (!action) return; + if (save && !await saveCurrentDocument()) return; + dispatch({ type: 'file_action' }); + if (action.kind === 'open') await openFile(action.path); + else discardCurrentDocument(); + }, [discardCurrentDocument, openFile, saveCurrentDocument, state.pendingFileAction]); + return { ...state, dirty, @@ -457,10 +490,12 @@ export function useDesktopWorkspace() { metricsSummary, selectedPath, tree, + cancelPendingFileAction: () => dispatch({ type: 'file_action' }), cancelPendingWorkspaceAction: () => dispatch({ type: 'workspace_action' }), clearRecent, clearGraphSelection: () => dispatch({ type: 'graph_selected' }), closeCurrentDocument, + finishPendingFileAction, finishPendingWorkspaceAction, flushGraphSettings, openRecentWorkspace: (path: string) => requestWorkspaceAction({ kind: 'open', path }), From 56bea92150a528ecf499fd02bb5ff0b0f2e917d9 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 08:08:29 -0700 Subject: [PATCH 17/25] fix(desktop): open large workspaces reliably --- .changeset/quiet-desktop-graph.md | 2 +- apps/desktop/scripts/core-sidecar.mjs | 13 +- apps/desktop/src-tauri/src/lib.rs | 10 +- apps/desktop/src/core-sidecar.test.ts | 112 ++++++++++++++++- .../src/graphCache/database/query/read.ts | 24 ++++ .../src/graphCache/database/records/parser.ts | 117 ++++++++++++++---- .../src/graphCache/database/records/types.ts | 5 + .../core/src/graphCache/database/snapshot.ts | 55 +++++++- .../core/src/graphCache/database/storage.ts | 10 ++ packages/core/src/workspace/queryGraph.ts | 42 ++++++- packages/core/src/workspace/requestGraph.ts | 14 +-- .../database/records/parser.test.ts | 78 +++++++++++- .../graphCache/database/snapshot.test.ts | 38 +++++- .../core/tests/workspace/requestGraph.test.ts | 38 ++++++ 14 files changed, 506 insertions(+), 52 deletions(-) diff --git a/.changeset/quiet-desktop-graph.md b/.changeset/quiet-desktop-graph.md index a90e96586..eff750638 100644 --- a/.changeset/quiet-desktop-graph.md +++ b/.changeset/quiet-desktop-graph.md @@ -2,4 +2,4 @@ "@codegraphy-dev/core": patch --- -Expose a cached CodeGraphy Workspace Relationship Graph request for local interfaces. +Expose a cached CodeGraphy Workspace Relationship Graph request for local interfaces, with a focused File and Folder projection that avoids hydrating unused analysis facts. diff --git a/apps/desktop/scripts/core-sidecar.mjs b/apps/desktop/scripts/core-sidecar.mjs index 76dd8a244..90acda256 100644 --- a/apps/desktop/scripts/core-sidecar.mjs +++ b/apps/desktop/scripts/core-sidecar.mjs @@ -2,6 +2,13 @@ import { createInterface } from 'node:readline'; import { pathToFileURL } from 'node:url'; +import { format } from 'node:util'; + +for (const method of ['debug', 'error', 'info', 'log', 'warn']) { + console[method] = (...values) => { + process.stderr.write(`${format(...values)}\n`); + }; +} const coreModuleUrl = process.env.CODEGRAPHY_DESKTOP_CORE_MODULE ? pathToFileURL(process.env.CODEGRAPHY_DESKTOP_CORE_MODULE) @@ -152,11 +159,9 @@ async function runRequest(request) { event: 'indexing', workspaceRoot: request.workspaceRoot, }); - const result = await core.indexCodeGraphyWorkspace({ - workspaceRoot: request.workspaceRoot, - }); + const record = startWorkspaceEngine(request.workspaceRoot); + const result = await record.ready; const response = indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); - warmWorkspaceEngine(request.workspaceRoot); return response; } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 2408a3d07..4048ae48d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -568,8 +568,13 @@ async fn request_core( while let Some(event) = process.events.recv().await { match event { CommandEvent::Stdout(line) => { - let message: CoreMessage = serde_json::from_slice(&line) - .map_err(|error| format!("Core service returned invalid data: {error}"))?; + let message: CoreMessage = match serde_json::from_slice(&line) { + Ok(message) => message, + Err(error) => { + state.process = None; + return Err(format!("Core service returned invalid data: {error}")); + } + }; match message { CoreMessage::Event { event, mut details } => { details.insert("event".to_string(), Value::String(event)); @@ -583,6 +588,7 @@ async fn request_core( }; } CoreMessage::Response { .. } => { + state.process = None; return Err("Core service returned a mismatched response.".to_string()); } } diff --git a/apps/desktop/src/core-sidecar.test.ts b/apps/desktop/src/core-sidecar.test.ts index 53082e815..5aab0f277 100644 --- a/apps/desktop/src/core-sidecar.test.ts +++ b/apps/desktop/src/core-sidecar.test.ts @@ -1,4 +1,4 @@ -import { execFileSync } from 'node:child_process'; +import { execFileSync, spawnSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -13,6 +13,116 @@ afterEach(() => { }); describe('Core desktop sidecar', () => { + it('keeps Core and plugin diagnostics out of the JSON protocol', () => { + const temporaryDirectory = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-sidecar-console-')); + temporaryDirectories.push(temporaryDirectory); + const coreModulePath = path.join(temporaryDirectory, 'core.mjs'); + writeFileSync(coreModulePath, ` + console.debug('[plugin] debug'); + console.error('[plugin] error'); + console.info('[plugin] info'); + console.log('[plugin] initialized'); + console.warn('[plugin] warning'); + export const requestCodeGraphyWorkspaceGraph = input => { + console.info('[core] opening %s', input.workspacePath); + return { + kind: 'ready', + workspaceRoot: input.workspacePath, + graphCache: '.codegraphy/graph.sqlite', + cacheStatus: { state: 'fresh', staleReasons: [] }, + graph: { nodes: [], edges: [] }, + }; + }; + export const createCodeGraphyWorkspaceEngine = () => ({ + index: async () => ({ files: [], totalFound: 0, limitReached: false }), + dispose: () => undefined, + }); + `); + const request = JSON.stringify({ + kind: 'request', + id: 1, + method: 'open', + params: { workspaceRoot: temporaryDirectory }, + }); + + const result = spawnSync(process.execPath, ['scripts/core-sidecar.mjs'], { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, CODEGRAPHY_DESKTOP_CORE_MODULE: coreModulePath }, + input: `${request}\n`, + }); + + expect(result.status).toBe(0); + expect(result.stdout.trim().split('\n').map(line => JSON.parse(line))).toEqual([ + expect.objectContaining({ kind: 'response', id: 1, outcome: 'success' }), + ]); + expect(result.stderr).toContain('[plugin] initialized'); + expect(result.stderr).toContain('[plugin] debug'); + expect(result.stderr).toContain('[plugin] error'); + expect(result.stderr).toContain('[plugin] info'); + expect(result.stderr).toContain('[plugin] warning'); + expect(result.stderr).toContain(`[core] opening ${temporaryDirectory}`); + }); + + it('keeps the first Indexing engine instead of indexing a cold workspace twice', () => { + const temporaryDirectory = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-sidecar-cold-')); + temporaryDirectories.push(temporaryDirectory); + const logPath = path.join(temporaryDirectory, 'engine.log'); + const coreModulePath = path.join(temporaryDirectory, 'core.mjs'); + writeFileSync(coreModulePath, ` + import { appendFileSync } from 'node:fs'; + const log = value => appendFileSync(${JSON.stringify(logPath)}, value + '\\n'); + let indexed = false; + const result = workspaceRoot => ({ + workspaceRoot, + files: [{ relativePath: 'README.md' }], + totalFound: 1, + limitReached: false, + indexing: { mode: 'full', analyzedFiles: 1, deletedFiles: 0, reusedFiles: 0 }, + }); + export const requestCodeGraphyWorkspaceGraph = input => indexed + ? { + kind: 'ready', + workspaceRoot: input.workspacePath, + graphCache: '.codegraphy/graph.sqlite', + cacheStatus: { state: 'fresh', staleReasons: [] }, + graph: { nodes: [], edges: [] }, + } + : { kind: 'missing', workspaceRoot: input.workspacePath, graphCache: '.codegraphy/graph.sqlite' }; + export const createCodeGraphyWorkspaceEngine = options => ({ + index: async () => { indexed = true; log('engine:index'); return result(options.workspaceRoot); }, + applyChangedFiles: async paths => { log('apply:' + paths.join(',')); return result(options.workspaceRoot); }, + dispose: () => log('dispose'), + }); + export const indexCodeGraphyWorkspace = async () => { + log('one-shot:index'); + throw new Error('The sidecar must not create a second Indexing lifecycle.'); + }; + `); + const input = [ + { kind: 'request', id: 1, method: 'open', params: { workspaceRoot: temporaryDirectory } }, + { kind: 'request', id: 2, method: 'update', params: { workspaceRoot: temporaryDirectory, relativePath: 'README.md' } }, + ].map(request => JSON.stringify(request)).join('\n'); + + const output = execFileSync(process.execPath, ['scripts/core-sidecar.mjs'], { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, CODEGRAPHY_DESKTOP_CORE_MODULE: coreModulePath }, + input: `${input}\n`, + }); + const responses = output.trim().split('\n') + .map(line => JSON.parse(line) as { kind: string; id?: number }) + .filter(message => message.kind === 'response'); + + expect(responses.map(response => response.id)).toEqual([1, 2]); + expect(readFileSync(logPath, 'utf8')).toBe([ + 'engine:index', + 'apply:README.md', + 'dispose', + '', + ].join('\n')); + }); + it('keeps a workspace engine and applies a saved File as an incremental change', () => { const temporaryDirectory = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-sidecar-test-')); temporaryDirectories.push(temporaryDirectory); diff --git a/packages/core/src/graphCache/database/query/read.ts b/packages/core/src/graphCache/database/query/read.ts index 82193aba6..42b325521 100644 --- a/packages/core/src/graphCache/database/query/read.ts +++ b/packages/core/src/graphCache/database/query/read.ts @@ -6,6 +6,13 @@ export const NODE_ROWS_QUERY = `SELECT Node.*, File.path AS filePath, Parent.key LEFT JOIN Node AS Parent ON Parent.id = Node.parentId ORDER BY Node.key`; +export const FILE_NODE_ROWS_QUERY = `SELECT Node.*, File.path AS filePath, Parent.key AS parentKey + FROM Node + LEFT JOIN File ON File.id = Node.fileId + LEFT JOIN Node AS Parent ON Parent.id = Node.parentId + WHERE Node.type = 'file' + ORDER BY Node.key`; + export const SYMBOL_ROWS_QUERY = `SELECT Symbol.*, Node.key AS nodeKey, File.path AS ownerFilePath FROM Symbol JOIN Node ON Node.id = Symbol.nodeId @@ -22,3 +29,20 @@ export const EDGE_ROWS_QUERY = `SELECT Edge.*, Source.key AS sourceNodeKey, LEFT JOIN File AS SourceFile ON SourceFile.id = Source.fileId LEFT JOIN File AS TargetFile ON TargetFile.id = Target.fileId ORDER BY Edge.key`; + +export const FILE_GRAPH_EDGE_ROWS_QUERY = `SELECT Edge.key, Edge.type, + Source.key AS originalSourceNodeKey, Source.type AS sourceNodeType, + CASE WHEN Source.type = 'file' THEN Source.key ELSE SourceFile.path END AS sourceNodeKey, + Target.key AS originalTargetNodeKey, Target.type AS targetNodeType, + CASE WHEN Target.type = 'file' THEN Target.key ELSE TargetFile.path END AS targetNodeKey + FROM Edge + JOIN Node AS Source ON Source.id = Edge.sourceNodeId + JOIN Node AS Target ON Target.id = Edge.targetNodeId + LEFT JOIN File AS SourceFile ON SourceFile.id = Source.fileId + LEFT JOIN File AS TargetFile ON TargetFile.id = Target.fileId + LEFT JOIN Symbol AS SourceSymbol ON SourceSymbol.nodeId = Source.id + LEFT JOIN Symbol AS TargetSymbol ON TargetSymbol.nodeId = Target.id + WHERE Edge.type <> 'contains' + AND (Source.type = 'file' OR SourceSymbol.nodeId IS NOT NULL) + AND (Target.type = 'file' OR TargetSymbol.nodeId IS NOT NULL) + ORDER BY Edge.key`; diff --git a/packages/core/src/graphCache/database/records/parser.ts b/packages/core/src/graphCache/database/records/parser.ts index 2b4d89cda..49edf1cab 100644 --- a/packages/core/src/graphCache/database/records/parser.ts +++ b/packages/core/src/graphCache/database/records/parser.ts @@ -1,5 +1,11 @@ import type { IAnalysisRelation, IAnalysisSymbol, IGraphData } from '@codegraphy-dev/plugin-api'; -import type { FileRow, GraphEdgeRow, GraphNodeRow, SymbolRow } from './types'; +import type { + FileGraphEdgeRow, + FileRow, + GraphEdgeRow, + GraphNodeRow, + SymbolRow, +} from './types'; import { createSnapshotFileEntry, type SnapshotFileEntry } from './file'; import { createSnapshotAnalysisNode, @@ -24,17 +30,11 @@ function appendGroupedRow(groups: Map, key: string | undefin groups.set(key, rows); } -export function parseDatabaseRecords( - fileRows: readonly FileRow[], +function hydrateGraphRows( nodeRows: readonly GraphNodeRow[], symbolRows: readonly SymbolRow[], edgeRows: readonly GraphEdgeRow[], - workspaceRoot: string, -): HydratedDatabaseRecords { - const files = fileRows.flatMap(row => { - const entry = createSnapshotFileEntry(row, workspaceRoot); - return entry ? [entry] : []; - }); +) { const hydratedNodeRows = nodeRows.map(row => ({ ...row, filePath: readOptionalString(row.filePath) ?? readOptionalString(row.fileId), @@ -44,10 +44,6 @@ export function parseDatabaseRecords( const key = readOptionalString(row.key); return key ? [[key, row] as const] : []; })); - const nodeRowsByFilePath = new Map(); - for (const row of hydratedNodeRows) { - appendGroupedRow(nodeRowsByFilePath, readOptionalString(row.filePath), row); - } const symbolRowsByFilePath = new Map(); const symbolRowsByNodeKey = new Map(); for (const row of symbolRows) { @@ -76,6 +72,85 @@ export function parseDatabaseRecords( targetFilePath: readOptionalString(row.targetFilePath) ?? readOptionalString(targetNode?.filePath), }; }); + return { hydratedEdgeRows, hydratedNodeRows, symbolRowsByFilePath, symbolRowsByNodeKey }; +} + +function createGraphFromHydratedRows( + hydratedNodeRows: readonly GraphNodeRow[], + hydratedEdgeRows: readonly GraphEdgeRow[], + symbolRowsByNodeKey: ReadonlyMap, +): IGraphData { + const nodes = hydratedNodeRows.flatMap(row => { + const key = readOptionalString(row.key); + const node = createSnapshotGraphNode(row, key ? symbolRowsByNodeKey.get(key) : undefined); + return node ? [node] : []; + }); + const edges = hydratedEdgeRows.flatMap(row => { + const edge = createSnapshotGraphEdge(row); + return edge ? [edge] : []; + }); + return { nodes, edges }; +} + +export function parseDatabaseGraphRecords( + nodeRows: readonly GraphNodeRow[], + symbolRows: readonly SymbolRow[], + edgeRows: readonly GraphEdgeRow[], +): IGraphData { + const hydrated = hydrateGraphRows(nodeRows, symbolRows, edgeRows); + return createGraphFromHydratedRows( + hydrated.hydratedNodeRows, + hydrated.hydratedEdgeRows, + hydrated.symbolRowsByNodeKey, + ); +} + +export function parseDatabaseFileGraphRecords( + nodeRows: readonly GraphNodeRow[], + edgeRows: readonly FileGraphEdgeRow[], +): IGraphData { + const nodes = nodeRows.flatMap(row => { + const node = createSnapshotGraphNode(row); + return node ? [node] : []; + }); + const edges = edgeRows.flatMap(row => { + const edge = createSnapshotGraphEdge(row); + if (!edge) return []; + const sourceWasFile = readOptionalString(row.originalSourceNodeKey) === edge.from; + const targetWasFile = readOptionalString(row.originalTargetNodeKey) === edge.to; + if (sourceWasFile && targetWasFile) return [edge]; + if (edge.from === edge.to) return []; + const suffixStart = edge.id.lastIndexOf('#'); + const suffix = suffixStart >= 0 ? edge.id.slice(suffixStart) : `#${edge.kind}`; + return [{ + ...edge, + id: `${edge.from}->${edge.to}${suffix}`, + }]; + }); + return { nodes, edges }; +} + +export function parseDatabaseRecords( + fileRows: readonly FileRow[], + nodeRows: readonly GraphNodeRow[], + symbolRows: readonly SymbolRow[], + edgeRows: readonly GraphEdgeRow[], + workspaceRoot: string, +): HydratedDatabaseRecords { + const files = fileRows.flatMap(row => { + const entry = createSnapshotFileEntry(row, workspaceRoot); + return entry ? [entry] : []; + }); + const { + hydratedEdgeRows, + hydratedNodeRows, + symbolRowsByFilePath, + symbolRowsByNodeKey, + } = hydrateGraphRows(nodeRows, symbolRows, edgeRows); + const nodeRowsByFilePath = new Map(); + for (const row of hydratedNodeRows) { + appendGroupedRow(nodeRowsByFilePath, readOptionalString(row.filePath), row); + } const edgeRowsBySourceFilePath = new Map(); for (const row of hydratedEdgeRows) { appendGroupedRow(edgeRowsBySourceFilePath, readOptionalString(row.sourceFilePath), row); @@ -105,14 +180,10 @@ export function parseDatabaseRecords( relations.push(...analysisRelations); } - const nodes = hydratedNodeRows.flatMap(row => { - const key = readOptionalString(row.key); - const node = createSnapshotGraphNode(row, key ? symbolRowsByNodeKey.get(key) : undefined); - return node ? [node] : []; - }); - const edges = hydratedEdgeRows.flatMap(row => { - const edge = createSnapshotGraphEdge(row); - return edge ? [edge] : []; - }); - return { files, graph: { nodes, edges }, symbols, relations }; + const graph = createGraphFromHydratedRows( + hydratedNodeRows, + hydratedEdgeRows, + symbolRowsByNodeKey, + ); + return { files, graph, symbols, relations }; } diff --git a/packages/core/src/graphCache/database/records/types.ts b/packages/core/src/graphCache/database/records/types.ts index 3db747216..7c5637672 100644 --- a/packages/core/src/graphCache/database/records/types.ts +++ b/packages/core/src/graphCache/database/records/types.ts @@ -98,3 +98,8 @@ export interface GraphEdgeRow { targetFilePath?: unknown; type?: unknown; } + +export interface FileGraphEdgeRow extends GraphEdgeRow { + originalSourceNodeKey?: unknown; + originalTargetNodeKey?: unknown; +} diff --git a/packages/core/src/graphCache/database/snapshot.ts b/packages/core/src/graphCache/database/snapshot.ts index 4aa0fc982..f04b8968e 100644 --- a/packages/core/src/graphCache/database/snapshot.ts +++ b/packages/core/src/graphCache/database/snapshot.ts @@ -10,9 +10,26 @@ import { withReadOnlyConnection, } from './io/connection'; import { getWorkspaceAnalysisDatabasePath } from './io/paths'; -import { parseDatabaseRecords } from './records/parser'; -import type { FileRow, GraphEdgeRow, GraphNodeRow, SymbolRow } from './records/types'; -import { EDGE_ROWS_QUERY, FILE_ROWS_QUERY, NODE_ROWS_QUERY, SYMBOL_ROWS_QUERY } from './query/read'; +import { + parseDatabaseFileGraphRecords, + parseDatabaseGraphRecords, + parseDatabaseRecords, +} from './records/parser'; +import type { + FileGraphEdgeRow, + FileRow, + GraphEdgeRow, + GraphNodeRow, + SymbolRow, +} from './records/types'; +import { + EDGE_ROWS_QUERY, + FILE_GRAPH_EDGE_ROWS_QUERY, + FILE_NODE_ROWS_QUERY, + FILE_ROWS_QUERY, + NODE_ROWS_QUERY, + SYMBOL_ROWS_QUERY, +} from './query/read'; export interface WorkspaceAnalysisDatabaseSnapshot { files: Array<{ @@ -33,6 +50,38 @@ const EMPTY_SNAPSHOT: WorkspaceAnalysisDatabaseSnapshot = { symbols: [], relations: [], }; +const EMPTY_GRAPH: IGraphData = { nodes: [], edges: [] }; + +export function readWorkspaceAnalysisDatabaseGraph(workspaceRoot: string): IGraphData { + const databasePath = getWorkspaceAnalysisDatabasePath(workspaceRoot); + if (!fs.existsSync(databasePath)) return EMPTY_GRAPH; + + try { + return withReadOnlyConnection(databasePath, connection => parseDatabaseGraphRecords( + readRowsSync(connection, NODE_ROWS_QUERY) as GraphNodeRow[], + readRowsSync(connection, SYMBOL_ROWS_QUERY) as SymbolRow[], + readRowsSync(connection, EDGE_ROWS_QUERY) as GraphEdgeRow[], + )); + } catch (error) { + console.warn('[CodeGraphy] Failed to read structured Relationship Graph.', error); + return EMPTY_GRAPH; + } +} + +export function readWorkspaceAnalysisDatabaseFileGraph(workspaceRoot: string): IGraphData { + const databasePath = getWorkspaceAnalysisDatabasePath(workspaceRoot); + if (!fs.existsSync(databasePath)) return EMPTY_GRAPH; + + try { + return withReadOnlyConnection(databasePath, connection => parseDatabaseFileGraphRecords( + readRowsSync(connection, FILE_NODE_ROWS_QUERY) as GraphNodeRow[], + readRowsSync(connection, FILE_GRAPH_EDGE_ROWS_QUERY) as FileGraphEdgeRow[], + )); + } catch (error) { + console.warn('[CodeGraphy] Failed to read the File Relationship Graph.', error); + return EMPTY_GRAPH; + } +} export function readWorkspaceAnalysisDatabaseSnapshot( workspaceRoot: string, diff --git a/packages/core/src/graphCache/database/storage.ts b/packages/core/src/graphCache/database/storage.ts index 076b977c9..29fad262d 100644 --- a/packages/core/src/graphCache/database/storage.ts +++ b/packages/core/src/graphCache/database/storage.ts @@ -13,6 +13,8 @@ import { readWorkspaceAnalysisDatabaseRecordCounts as readWorkspaceAnalysisDatabaseRecordCountsImpl, } from './recordCounts/model'; import { + readWorkspaceAnalysisDatabaseFileGraph as readWorkspaceAnalysisDatabaseFileGraphImpl, + readWorkspaceAnalysisDatabaseGraph as readWorkspaceAnalysisDatabaseGraphImpl, readWorkspaceAnalysisDatabaseSnapshot as readWorkspaceAnalysisDatabaseSnapshotImpl, type WorkspaceAnalysisDatabaseSnapshot as WorkspaceAnalysisDatabaseSnapshotImpl, } from './snapshot'; @@ -72,6 +74,14 @@ export function readWorkspaceAnalysisDatabaseSnapshot( return readWorkspaceAnalysisDatabaseSnapshotImpl(workspaceRoot); } +export function readWorkspaceAnalysisDatabaseGraph(workspaceRoot: string) { + return readWorkspaceAnalysisDatabaseGraphImpl(workspaceRoot); +} + +export function readWorkspaceAnalysisDatabaseFileGraph(workspaceRoot: string) { + return readWorkspaceAnalysisDatabaseFileGraphImpl(workspaceRoot); +} + export function clearWorkspaceAnalysisDatabaseCache( workspaceRoot: string, ): void { diff --git a/packages/core/src/workspace/queryGraph.ts b/packages/core/src/workspace/queryGraph.ts index 4cf33c7a1..3defae3fc 100644 --- a/packages/core/src/workspace/queryGraph.ts +++ b/packages/core/src/workspace/queryGraph.ts @@ -2,7 +2,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { createHash } from 'node:crypto'; import { StringDecoder } from 'node:string_decoder'; -import { readWorkspaceAnalysisDatabaseSnapshot } from '../graphCache/database/storage'; +import { + readWorkspaceAnalysisDatabaseGraph, + readWorkspaceAnalysisDatabaseFileGraph, + readWorkspaceAnalysisDatabaseSnapshot, +} from '../graphCache/database/storage'; import { filterInactivePluginSnapshotFacts } from '../plugins/activityState/analysisFacts'; import { createPluginActivityState } from '../plugins/activityState/model'; import type { CodeGraphyInstalledPluginCache } from '../plugins/installedCache'; @@ -167,8 +171,13 @@ export function readWorkspaceQuerySource( }; } -export function projectWorkspaceQueryGraph( - source: ReturnType, +type WorkspaceGraphProjectionSource = Pick< + ReturnType, + 'declarations' | 'graphData' | 'settings' +>; + +function projectWorkspaceGraph( + source: WorkspaceGraphProjectionSource, projection: WorkspaceGraphQueryProjection = {}, ) { const disabledFilterPatterns = new Set(source.settings.disabledCustomFilterPatterns); @@ -201,10 +210,35 @@ export function projectWorkspaceQueryGraph( nodeTypes: source.declarations.nodes, scope, settings: source.settings, - snapshotFacts: filterSnapshotFactsToGraph(source.snapshotFacts, graphData), }; } +export function projectWorkspaceQueryGraph( + source: ReturnType, + projection: WorkspaceGraphQueryProjection = {}, +) { + const projected = projectWorkspaceGraph(source, projection); + return { + ...projected, + snapshotFacts: filterSnapshotFactsToGraph(source.snapshotFacts, projected.graphData), + }; +} + +export function readWorkspaceProjectedGraph( + workspaceRoot: string, + projection: WorkspaceGraphQueryProjection = {}, +) { + const fileGraphOnly = projection.nodeTypes?.length + && projection.nodeTypes.every(nodeType => nodeType === 'file' || nodeType === 'folder'); + return projectWorkspaceGraph({ + declarations: { nodes: [], edges: [] }, + graphData: fileGraphOnly + ? readWorkspaceAnalysisDatabaseFileGraph(workspaceRoot) + : readWorkspaceAnalysisDatabaseGraph(workspaceRoot), + settings: readCodeGraphyWorkspaceSettings(workspaceRoot), + }, projection); +} + export function readWorkspaceQueryGraph( workspaceRoot: string, installedPluginCache: CodeGraphyInstalledPluginCache, diff --git a/packages/core/src/workspace/requestGraph.ts b/packages/core/src/workspace/requestGraph.ts index b983ec998..025c8c4b8 100644 --- a/packages/core/src/workspace/requestGraph.ts +++ b/packages/core/src/workspace/requestGraph.ts @@ -1,12 +1,8 @@ import * as path from 'node:path'; import { inspectWorkspaceAnalysisDatabase } from '../graphCache/database/storage'; import type { IGraphData } from '../graph/contracts'; -import { - readCodeGraphyInstalledPluginCache, - type CodeGraphyInstalledPluginCache, -} from '../plugins/installedCache'; import { deriveVisibleGraph } from '../visibleGraph'; -import { readWorkspaceQueryGraph } from './queryGraph'; +import { readWorkspaceProjectedGraph } from './queryGraph'; import { resolveCodeGraphyWorkspacePath } from './requestPaths'; import type { WorkspaceGraphQueryProjection, WorkspacePathInput } from './requestTypes'; import { readCodeGraphyWorkspaceStatus } from './status'; @@ -40,12 +36,10 @@ export type WorkspaceGraphResult = export interface WorkspaceGraphDependencies { cwd(): string; - readInstalledPluginCache(): CodeGraphyInstalledPluginCache; } const DEFAULT_DEPENDENCIES: WorkspaceGraphDependencies = { cwd: () => process.cwd(), - readInstalledPluginCache: () => readCodeGraphyInstalledPluginCache(), }; /** @@ -72,11 +66,7 @@ export function requestCodeGraphyWorkspaceGraph( }; } - const projected = readWorkspaceQueryGraph( - workspaceRoot, - dependencies.readInstalledPluginCache(), - input.projection, - ); + const projected = readWorkspaceProjectedGraph(workspaceRoot, input.projection); const graph = deriveVisibleGraph(projected.graphData, { scope: { nodes: Object.entries(projected.scope.nodes).map(([type, enabled]) => ({ type, enabled })), diff --git a/packages/core/tests/graphCache/database/records/parser.test.ts b/packages/core/tests/graphCache/database/records/parser.test.ts index d30142690..7d491e6db 100644 --- a/packages/core/tests/graphCache/database/records/parser.test.ts +++ b/packages/core/tests/graphCache/database/records/parser.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from 'vitest'; import type { IFileAnalysisResult } from '@codegraphy-dev/plugin-api'; import type { IWorkspaceAnalysisCache } from '../../../../src/analysis/cache'; -import { parseDatabaseRecords } from '../../../../src/graphCache/database/records/parser'; +import { + parseDatabaseFileGraphRecords, + parseDatabaseGraphRecords, + parseDatabaseRecords, +} from '../../../../src/graphCache/database/records/parser'; import { serializeDatabaseRecords } from '../../../../src/graphCache/database/records/serializer'; describe('graphCache/database/parser', () => { @@ -65,6 +69,78 @@ describe('graphCache/database/parser', () => { expect(hydrated.graph.edges.map(edge => edge.kind)).not.toContain('codegraphy:has-cache-tier'); }); + it('reads the same canonical Relationship Graph without hydrating analysis facts', () => { + const cache: IWorkspaceAnalysisCache = { + version: '1', + files: { + 'src/a.ts': { + mtime: 1, + analysis: { + filePath: '/workspace/src/a.ts', + relations: [{ + kind: 'import', + sourceId: 'a-imports-b', + fromFilePath: '/workspace/src/a.ts', + toFilePath: '/workspace/src/b.ts', + }], + }, + }, + 'src/b.ts': { mtime: 1, analysis: { filePath: '/workspace/src/b.ts' } }, + }, + }; + const records = serializeDatabaseRecords(cache); + + expect(parseDatabaseGraphRecords(records.nodes, records.symbols, records.edges)).toEqual( + parseDatabaseRecords( + records.files, + records.nodes, + records.symbols, + records.edges, + '/workspace', + ).graph, + ); + }); + + it('projects Symbol Relationships to Files while preserving File Relationships', () => { + const graph = parseDatabaseFileGraphRecords( + [ + { key: 'a.ts', type: 'file', label: 'a.ts', filePath: 'a.ts' }, + { key: 'b.ts', type: 'file', label: 'b.ts', filePath: 'b.ts' }, + ], + [ + { + key: 'original-file-edge', + originalSourceNodeKey: 'a.ts', + sourceNodeKey: 'a.ts', + originalTargetNodeKey: 'b.ts', + targetNodeKey: 'b.ts', + type: 'import', + }, + { + key: 'a.ts#run->b.ts#run#call', + originalSourceNodeKey: 'a.ts#run', + sourceNodeKey: 'a.ts', + originalTargetNodeKey: 'b.ts#run', + targetNodeKey: 'b.ts', + type: 'call', + }, + { + key: 'a.ts#one->a.ts#two#call', + originalSourceNodeKey: 'a.ts#one', + sourceNodeKey: 'a.ts', + originalTargetNodeKey: 'a.ts#two', + targetNodeKey: 'a.ts', + type: 'call', + }, + ], + ); + + expect(graph.edges).toEqual([ + expect.objectContaining({ id: 'original-file-edge', from: 'a.ts', to: 'b.ts' }), + expect.objectContaining({ id: 'a.ts->b.ts#call', from: 'a.ts', to: 'b.ts' }), + ]); + }); + it('hydrates one canonical Edge row without serialized provenance', () => { const hydrated = parseDatabaseRecords( [], diff --git a/packages/core/tests/graphCache/database/snapshot.test.ts b/packages/core/tests/graphCache/database/snapshot.test.ts index efb8fe024..6b1daaa4d 100644 --- a/packages/core/tests/graphCache/database/snapshot.test.ts +++ b/packages/core/tests/graphCache/database/snapshot.test.ts @@ -1,6 +1,9 @@ import * as fs from 'node:fs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { readWorkspaceAnalysisDatabaseSnapshot } from '../../../src/graphCache/database/snapshot'; +import { + readWorkspaceAnalysisDatabaseGraph, + readWorkspaceAnalysisDatabaseSnapshot, +} from '../../../src/graphCache/database/snapshot'; import * as connectionModule from '../../../src/graphCache/database/io/connection'; import * as pathModule from '../../../src/graphCache/database/io/paths'; import { @@ -75,6 +78,39 @@ describe('graphCache/database/snapshot', () => { }); }); + it('reads only canonical graph records for a projected Relationship Graph', () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(connectionModule.withReadOnlyConnection).mockImplementation( + (_path, callback) => callback('connection' as never), + ); + vi.mocked(connectionModule.readRowsSync).mockImplementation((_connection, query) => { + if (query === NODE_ROWS_QUERY) return [ + { id: 1, key: 'a.ts', type: 'file', label: 'a.ts', filePath: 'a.ts' }, + { id: 2, key: 'b.ts', type: 'file', label: 'b.ts', filePath: 'b.ts' }, + ]; + if (query === SYMBOL_ROWS_QUERY) return []; + if (query === EDGE_ROWS_QUERY) return [{ + key: 'a.ts->b.ts#import', + sourceNodeKey: 'a.ts', + targetNodeKey: 'b.ts', + type: 'import', + }]; + throw new Error(`Unexpected query: ${query}`); + }); + + expect(readWorkspaceAnalysisDatabaseGraph('/workspace')).toEqual({ + nodes: [ + expect.objectContaining({ id: 'a.ts', nodeType: 'file' }), + expect.objectContaining({ id: 'b.ts', nodeType: 'file' }), + ], + edges: [expect.objectContaining({ id: 'a.ts->b.ts#import', kind: 'import' })], + }); + expect(connectionModule.readRowsSync).not.toHaveBeenCalledWith( + expect.anything(), + FILE_ROWS_QUERY, + ); + }); + it('warns and returns an empty snapshot when reading fails', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(connectionModule.withReadOnlyConnection).mockImplementation(() => { throw new Error('broken'); }); diff --git a/packages/core/tests/workspace/requestGraph.test.ts b/packages/core/tests/workspace/requestGraph.test.ts index 1c2f6ac29..a655e5bbb 100644 --- a/packages/core/tests/workspace/requestGraph.test.ts +++ b/packages/core/tests/workspace/requestGraph.test.ts @@ -4,12 +4,50 @@ import * as path from 'node:path'; import { describe, expect, it } from 'vitest'; import { requestCodeGraphyIndexWorkspace } from '../../src/workspace/requestIndexing'; import { requestCodeGraphyWorkspaceGraph } from '../../src/workspace/requestGraph'; +import { readWorkspaceQueryGraph } from '../../src/workspace/queryGraph'; import { readCodeGraphyWorkspaceSettingsOrInitial, writeCodeGraphyWorkspaceSettings, } from '../../src/workspace/settings'; +import { deriveVisibleGraph } from '../../src/visibleGraph'; describe('workspace/requestGraph', () => { + it('keeps the File and Folder projection equal to the complete Core query path', async () => { + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codegraphy-file-graph-')); + await fs.mkdir(path.join(workspaceRoot, 'src')); + await fs.writeFile( + path.join(workspaceRoot, 'src', 'entry.ts'), + "import { modelValue } from './model';\nexport function entry() { return modelValue; }\n", + ); + await fs.writeFile( + path.join(workspaceRoot, 'src', 'model.ts'), + 'export const modelValue = 1;\n', + ); + await requestCodeGraphyIndexWorkspace({ workspacePath: workspaceRoot }); + const projection = { nodeTypes: ['file', 'folder'] }; + const complete = readWorkspaceQueryGraph( + workspaceRoot, + { version: 3, plugins: [] }, + projection, + ); + const expected = deriveVisibleGraph(complete.graphData, { + scope: { + nodes: Object.entries(complete.scope.nodes).map(([type, enabled]) => ({ type, enabled })), + edges: Object.entries(complete.scope.edges).map(([type, enabled]) => ({ type, enabled })), + nodeTypes: complete.nodeTypes, + }, + showOrphans: true, + }).graphData; + + const result = requestCodeGraphyWorkspaceGraph({ + workspacePath: workspaceRoot, + projection, + }); + + expect(result.kind).toBe('ready'); + if (result.kind === 'ready') expect(result.graph).toEqual(expected); + }); + it('loads the saved Core Graph Scope and Filters without running Indexing', async () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codegraphy-workspace-graph-')); await fs.writeFile( From 985862fa9f07d89c96b414257ba04f4f6e2db9b5 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 08:24:14 -0700 Subject: [PATCH 18/25] feat(desktop): deepen focused file navigation --- README.md | 5 +- apps/desktop/README.md | 11 +- apps/desktop/package.json | 6 +- apps/desktop/src/App.tsx | 69 +- apps/desktop/src/components/CodeEditor.css | 116 ++ .../src/components/CodeEditor.test.tsx | 85 ++ apps/desktop/src/components/CodeEditor.tsx | 89 +- apps/desktop/src/components/FileTree.test.tsx | 1 + apps/desktop/src/components/FileTree.tsx | 10 +- .../src/components/GraphPanel.test.tsx | 13 +- .../src/components/MarkdownPreview.test.tsx | 58 + .../src/components/MarkdownPreview.tsx | 51 + .../src/components/WorkspacePanes.test.tsx | 32 + .../desktop/src/components/WorkspacePanes.tsx | 22 +- .../src/components/createCodeEditor.ts | 30 +- .../src/components/editorLanguage.test.ts | 33 + apps/desktop/src/components/editorLanguage.ts | 15 + apps/desktop/src/components/markdownPath.ts | 3 + apps/desktop/src/interfacePreferences.test.ts | 46 + apps/desktop/src/interfacePreferences.ts | 50 + apps/desktop/src/styles.css | 2 + apps/desktop/src/useDesktopGraphRenderer.ts | 62 +- .../desktop-app-product-expectations.md | 183 --- docs/research/desktop-app-stack.md | 100 -- pnpm-lock.yaml | 1275 ++++++++++++++++- 25 files changed, 2018 insertions(+), 349 deletions(-) create mode 100644 apps/desktop/src/components/CodeEditor.css create mode 100644 apps/desktop/src/components/CodeEditor.test.tsx create mode 100644 apps/desktop/src/components/MarkdownPreview.test.tsx create mode 100644 apps/desktop/src/components/MarkdownPreview.tsx create mode 100644 apps/desktop/src/components/editorLanguage.test.ts create mode 100644 apps/desktop/src/components/editorLanguage.ts create mode 100644 apps/desktop/src/components/markdownPath.ts create mode 100644 apps/desktop/src/interfacePreferences.test.ts create mode 100644 apps/desktop/src/interfacePreferences.ts delete mode 100644 docs/research/desktop-app-product-expectations.md delete mode 100644 docs/research/desktop-app-stack.md diff --git a/README.md b/README.md index 7494e5adf..15f105b7b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Join the [CodeGraphy Discord](https://discord.gg/Z75vbkt4Ry) for installation he | Capability | What it provides | |---|---| | Relationship Graph | File, folder, package, Symbol, and plugin-defined Nodes connected by typed Edges. | -| macOS desktop app | A three-pane File hierarchy, CodeMirror editor, and WebGPU Relationship Graph backed by local Core. | +| macOS desktop app | A focused File hierarchy, multi-language CodeMirror editor with Markdown preview, and WebGPU Relationship Graph backed by local Core. | | Search and filters | Temporary search plus workspace-local include and exclude rules. | | Graph Scope | One panel for Node Type and Edge Type visibility. | | Symbol Nodes | Functions, classes, interfaces, types, variables, constants, and language-specific declarations. | @@ -208,7 +208,7 @@ A public `codegraphy/skills` repository will host the skill once published. The macOS desktop app runs Core in a bundled local process and keeps File access and child-process control in Rust. Its webview combines a File hierarchy, CodeMirror, and the existing renderer. The VS Code extension connects Core to the editor lifecycle and React Graph View. The tldraw interface connects Core data and shared physics to native tldraw shapes. -`@codegraphy-dev/graph-renderer` owns WebGPU drawing and WebAssembly physics. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. +`@codegraphy-dev/graph-renderer` owns product-neutral WebGPU drawing and WebAssembly physics. The private `@codegraphy-dev/graph-visuals` package owns CodeGraphy-specific Node appearance, Material icon matching, connection sizing, and force-control semantics shared by the desktop app and VS Code extension. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. | Package | Role | |---|---| @@ -217,6 +217,7 @@ The macOS desktop app runs Core in a bundled local process and keeps File access | [`@codegraphy-dev/extension`](./packages/extension/docs/README.md) | VS Code host and Graph View product integration. | | [`@codegraphy-dev/tldraw`](./packages/tldraw/README.md) | macOS launcher and native tldraw offline canvas integration. | | [`@codegraphy-dev/graph-renderer`](./packages/graph-renderer/README.md) | WebGPU graph renderer and WebAssembly physics. | +| `@codegraphy-dev/graph-visuals` | Private CodeGraphy visual and force-control semantics shared by interfaces. | | [`@codegraphy-dev/plugin-api`](./packages/plugin-api/README.md) | Public TypeScript contracts for Core plugins. | | [`@codegraphy-dev/extension-plugin-api`](./packages/extension-plugin-api/README.md) | Public TypeScript contracts for VS Code Extension plugins. | | `@codegraphy-dev/plugin-*` | Optional plugins for Core or an interface host. | diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 80a25ef32..c1f089422 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -16,8 +16,9 @@ Intel is not a supported release target yet. The graph renderer has no non-WebGP - Open any local folder as a CodeGraphy Workspace. - Switch workspaces from the toolbar or the native `File > Open Recent` menu. Missing recent folders stay visible as unavailable until the user clears the menu. -- Browse and filter a thin semantic File and Folder hierarchy. Arrow keys, Home, End, Enter, Space, type-ahead, `/`, and `Cmd+F` work while the hierarchy has focus. Opening a File keeps that focus for rapid switching. -- Open UTF-8 text Files up to 5 MiB in CodeMirror with the maintained One Dark highlight theme. Close File clears only the editor after the same dirty-edit confirmation used for switching Files. +- Browse and filter a thin semantic File and Folder hierarchy. Arrow keys, Home, End, Enter, Space, type-ahead, `/`, and `Cmd+F` work while the hierarchy has focus. Up, Down, Home, End, and Right open each focused File without moving focus into the editor, so keyboard switching stays continuous. +- Open UTF-8 text Files up to 5 MiB in CodeMirror with the maintained One Dark highlight theme and maintained language support for common source and data formats. Close File clears only the editor after the same dirty-edit confirmation used for switching Files. +- Edit Markdown as source, show a live rendered preview, or use a split source-and-preview view. Markdown preview code loads only for Markdown Files and stays independent of the Core plugin system. - Save through an atomic replacement that preserves permissions and rejects an external edit conflict. - Read or rebuild the workspace-owned `.codegraphy/graph.sqlite` Graph Cache through Core. - Apply one-File incremental Indexing after a save. @@ -25,6 +26,8 @@ Intel is not a supported release target yet. The graph renderer has no non-WebGP - Choose a File or Folder Node, drag any Node, pan the Graph Stage, zoom at the pointer, use Zoom In, Zoom Out, or Fit to Screen, and let the shared WebAssembly simulation settle after release. Clicking empty graph background clears graph selection without closing the editor File. - Read the live ` Nodes · Relationships` count for the displayed File and Folder graph. - Resize all three panes with pointer or keyboard separators. Pane proportions persist in local interface state and clamp to usable widths when the window changes. +- Hide the File hierarchy or Relationship Graph independently to focus on the open File. These interface preferences stay local and restore on the next launch. +- Show measured File-open latency only when Profile is enabled. The measurement starts before the Rust File read and ends after React, CodeMirror, and two painted frames; it reports the latest, median, and p95 sample instead of inferring speed from animation. - Tune Repel Force, Center Force, and Link Distance from 30 to 150 live from Graph Settings. Reset restores the extension defaults. The desktop record persists in the workspace without restarting Core or Indexing. Source Files and the Graph Cache stay in the workspace. The app does not upload them. @@ -33,9 +36,9 @@ Source Files and the Graph Cache stay in the workspace. The app does not upload The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. -Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-visuals` owns the visual and force-setting meaning shared with the extension. The right pane uses `@codegraphy-dev/graph-renderer` for WebGPU drawing and WebAssembly physics; desktop pointer handling stays in the desktop interface. +Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-visuals` owns CodeGraphy product policy shared with the extension: Material icon matching, Node appearance, connection sizing, and the named force-control contract. The smaller `@codegraphy-dev/graph-renderer` package remains product-neutral and owns only WebGPU drawing plus WebAssembly physics. Desktop pointer handling stays in the desktop interface. -See the [stack decision](../../docs/research/desktop-app-stack.md) for the comparison and release constraints. +This boundary keeps the existing TypeScript Core and WebGPU renderer intact while the small Rust shell provides native macOS integration and safe local File access. ## Development diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 982f65429..f5c9e349a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -24,8 +24,8 @@ "@codegraphy-dev/graph-renderer": "workspace:*", "@codegraphy-dev/graph-visuals": "workspace:*", "@codemirror/commands": "^6.10.4", - "@codemirror/lang-javascript": "^6.2.5", "@codemirror/language": "^6.12.4", + "@codemirror/language-data": "^6.5.2", "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.7.1", "@codemirror/theme-one-dark": "^6.1.3", @@ -34,7 +34,9 @@ "@tauri-apps/api": "^2.11.1", "material-icon-theme": "5.33.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@tauri-apps/cli": "^2.11.4", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 76879ba4b..4247421f7 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,4 +1,5 @@ import brandIconUrl from '../../../assets/icon-dark.svg?url'; +import { useState } from 'react'; import { CodeEditor } from './components/CodeEditor'; import { FileTree } from './components/FileTree'; import { GraphPanel } from './components/GraphPanel'; @@ -7,10 +8,30 @@ import { UnsavedFileDialog } from './components/UnsavedFileDialog'; import { WorkspacePanes } from './components/WorkspacePanes'; import { WorkspaceSwitcher } from './components/WorkspaceSwitcher'; import { formatGraphCounts } from './model'; +import { + readDesktopInterfacePreferences, + saveDesktopInterfacePreferences, + type DesktopInterfacePreferences, +} from './interfacePreferences'; import { useDesktopWorkspace } from './useDesktopWorkspace'; export function App(): React.ReactElement { const workspace = useDesktopWorkspace(); + const [interfacePreferences, setInterfacePreferences] = useState(() => ( + readDesktopInterfacePreferences(window.localStorage) + )); + const updateInterfacePreference = ( + key: Key, + value: DesktopInterfacePreferences[Key], + ): void => { + const next = { ...interfacePreferences, [key]: value }; + try { + saveDesktopInterfacePreferences(window.localStorage, next); + } catch { + // Keep the current window usable when the webview denies local persistence. + } + setInterfacePreferences(next); + }; return (
@@ -31,6 +52,42 @@ export function App(): React.ReactElement { workspaceRoot={workspace.workspaceRoot} />
+ + + + ))} +
+
+ }> + + + ) : null} +
+ + ); } diff --git a/apps/desktop/src/components/FileTree.test.tsx b/apps/desktop/src/components/FileTree.test.tsx index 36ba61f12..29862eb26 100644 --- a/apps/desktop/src/components/FileTree.test.tsx +++ b/apps/desktop/src/components/FileTree.test.tsx @@ -84,6 +84,7 @@ describe('File hierarchy keyboard and filter behavior', () => { expect(document.activeElement).toBe( host.querySelector('[data-tree-path="src/graph/camera.ts"]'), ); + expect(onSelect).toHaveBeenLastCalledWith('src/graph/camera.ts'); expect(host.querySelectorAll('[role="treeitem"][tabindex="0"]')).toHaveLength(1); await act(async () => root.unmount()); }); diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx index 1958fbb9d..9addde08b 100644 --- a/apps/desktop/src/components/FileTree.tsx +++ b/apps/desktop/src/components/FileTree.tsx @@ -169,6 +169,12 @@ export function FileTree({ }); }; + const focusAndOpenItem = (item: HTMLButtonElement | undefined): void => { + const path = item?.dataset.treePath; + focusPath(path); + if (path && item?.dataset.treeKind === 'file' && path !== selectedPath) onSelect(path); + }; + const togglePath = (path: string): void => { if (filterActive) return; setCollapsedPaths(current => { @@ -213,7 +219,7 @@ export function FileTree({ : event.key === 'End' ? items.length - 1 : Math.min(items.length - 1, Math.max(0, currentIndex + (event.key === 'ArrowDown' ? 1 : -1))); - focusPath(items[nextIndex]?.dataset.treePath); + focusAndOpenItem(items[nextIndex]); return; } @@ -222,7 +228,7 @@ export function FileTree({ event.preventDefault(); if (expanded === 'false') togglePath(event.target.dataset.treePath ?? ''); else if (visibleRows[currentIndex + 1]?.depth === (visibleRows[currentIndex]?.depth ?? 0) + 1) { - focusPath(items[currentIndex + 1]?.dataset.treePath); + focusAndOpenItem(items[currentIndex + 1]); } return; } diff --git a/apps/desktop/src/components/GraphPanel.test.tsx b/apps/desktop/src/components/GraphPanel.test.tsx index 2c3f7a341..c5790091f 100644 --- a/apps/desktop/src/components/GraphPanel.test.tsx +++ b/apps/desktop/src/components/GraphPanel.test.tsx @@ -245,10 +245,14 @@ describe('GraphPanel', () => { await act(async () => animationFrames.splice(0).forEach(callback => callback(32))); const zoomBeforeControl = renderedZoom(rendererMocks.render.mock.lastCall?.[0]); await act(async () => host.querySelector('[aria-label="Zoom In"]')?.click()); - await act(async () => animationFrames.splice(0).forEach(callback => callback(48))); + await act(async () => animationFrames.shift()?.(48)); + expect(renderedZoom(rendererMocks.render.mock.lastCall?.[0])).toBe(zoomBeforeControl); + await act(async () => animationFrames.shift()?.(128)); expect(renderedZoom(rendererMocks.render.mock.lastCall?.[0])).toBeGreaterThan(zoomBeforeControl); + await act(async () => animationFrames.shift()?.(224)); await act(async () => host.querySelector('[aria-label="Fit to Screen"]')?.click()); - await act(async () => animationFrames.splice(0).forEach(callback => callback(64))); + await act(async () => animationFrames.shift()?.(240)); + await act(async () => animationFrames.shift()?.(400)); expect(renderedZoom(rendererMocks.render.mock.lastCall?.[0])).toBe(zoomBeforeControl); await act(async () => root.unmount()); }); @@ -383,6 +387,11 @@ describe('GraphPanel', () => { backgroundColor: 'rgb(10, 11, 12)', }); expect(animationFrames).toHaveLength(0); + const zoomBefore = renderedZoom(rendererMocks.render.mock.lastCall?.[0]); + await act(async () => host.querySelector('[aria-label="Zoom In"]')?.click()); + await act(async () => animationFrames.shift()?.(16)); + expect(renderedZoom(rendererMocks.render.mock.lastCall?.[0])).toBeGreaterThan(zoomBefore); + expect(animationFrames).toHaveLength(0); await act(async () => root.unmount()); }); }); diff --git a/apps/desktop/src/components/MarkdownPreview.test.tsx b/apps/desktop/src/components/MarkdownPreview.test.tsx new file mode 100644 index 000000000..bf09a6fe9 --- /dev/null +++ b/apps/desktop/src/components/MarkdownPreview.test.tsx @@ -0,0 +1,58 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { MARKDOWN_PREVIEW_CHARACTER_LIMIT, MarkdownPreview } from './MarkdownPreview'; + +describe('MarkdownPreview', () => { + beforeEach(() => vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)); + + afterEach(() => { + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('renders GFM without executing raw HTML or exposing inert navigation controls', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + window.bad = true'} />, + )); + + expect(host.querySelector('h1')?.textContent).toBe('Guide'); + expect(host.querySelector('input[type="checkbox"]')?.checked).toBe(true); + expect(host.querySelector('script')).toBeNull(); + expect(host.textContent).not.toContain('window.bad'); + expect(host.querySelector('a')).toBeNull(); + expect(host.querySelector('.markdown-preview-link')?.textContent).toBe('CodeGraphy'); + + await act(async () => root.unmount()); + }); + + it('shows an honest limit state instead of rendering a pathological preview', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + , + )); + + expect(host.querySelector('[role="status"]')?.textContent).toContain('over 100,000 characters'); + expect(host.querySelector('article')).toBeNull(); + await act(async () => root.unmount()); + }); + + it('shows an honest placeholder for workspace-relative images', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + , + )); + + expect(host.querySelector('img')).toBeNull(); + expect(host.querySelector('.markdown-preview-image-placeholder')?.textContent) + .toBe('Image: Relationship Graph'); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/components/MarkdownPreview.tsx b/apps/desktop/src/components/MarkdownPreview.tsx new file mode 100644 index 000000000..0c29ff4c2 --- /dev/null +++ b/apps/desktop/src/components/MarkdownPreview.tsx @@ -0,0 +1,51 @@ +import ReactMarkdown from 'react-markdown'; +import type { Components } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +export const MARKDOWN_PREVIEW_CHARACTER_LIMIT = 100_000; + +const embeddedImagePattern = /^data:image\/(?:avif|gif|jpeg|png|webp);base64,/i; + +const markdownComponents: Components = { + ['a']: ({ children, href, ...props }) => ( + + {children} + + ), + ['img']: ({ alt, src }) => typeof src === 'string' && embeddedImagePattern.test(src) + ? {alt + : ( + + {alt ? `Image: ${alt}` : 'Image preview is not available yet'} + + ), +}; + +export function MarkdownPreview({ content }: { content: string }): React.ReactElement { + if (content.length > MARKDOWN_PREVIEW_CHARACTER_LIMIT) { + return ( +
+ Preview is disabled for Markdown Files over 100,000 characters to keep editing responsive. +
+ ); + } + + return ( +
+ + {content} + +
+ ); +} diff --git a/apps/desktop/src/components/WorkspacePanes.test.tsx b/apps/desktop/src/components/WorkspacePanes.test.tsx index 1a6f67491..1607d0c27 100644 --- a/apps/desktop/src/components/WorkspacePanes.test.tsx +++ b/apps/desktop/src/components/WorkspacePanes.test.tsx @@ -72,4 +72,36 @@ describe('Workspace pane splitters', () => { expect(JSON.parse(window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY) ?? 'null')).toEqual(DEFAULT_PANE_LAYOUT); await act(async () => root.unmount()); }); + + it('collapses either side pane without discarding the editor or saved proportions', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + const panes = (filesVisible: boolean, graphVisible: boolean) => ( + Editor} + filesPane={} + filesVisible={filesVisible} + graphPane={} + graphVisible={graphVisible} + /> + ); + + await act(async () => root.render(panes(false, true))); + expect(host.querySelector('[data-pane="files"]')).toBeNull(); + expect(host.querySelector('[data-pane="editor"]')).not.toBeNull(); + expect(host.querySelector('[data-pane="graph"]')).not.toBeNull(); + expect(host.querySelectorAll('[role="separator"]')).toHaveLength(1); + + await act(async () => root.render(panes(true, false))); + expect(host.querySelector('[data-pane="files"]')).not.toBeNull(); + expect(host.querySelector('[data-pane="graph"]')).toBeNull(); + expect(host.querySelectorAll('[role="separator"]')).toHaveLength(1); + + await act(async () => root.render(panes(false, false))); + expect(host.querySelector('[data-pane="editor"]')).not.toBeNull(); + expect(host.querySelectorAll('[role="separator"]')).toHaveLength(0); + expect(window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY)).toBeNull(); + await act(async () => root.unmount()); + }); }); diff --git a/apps/desktop/src/components/WorkspacePanes.tsx b/apps/desktop/src/components/WorkspacePanes.tsx index a1eb527d3..22daeac5e 100644 --- a/apps/desktop/src/components/WorkspacePanes.tsx +++ b/apps/desktop/src/components/WorkspacePanes.tsx @@ -34,11 +34,15 @@ function savePaneLayout(layout: PaneLayout): void { export function WorkspacePanes({ editorPane, filesPane, + filesVisible, graphPane, + graphVisible, }: { editorPane: React.ReactNode; filesPane: React.ReactNode; + filesVisible?: boolean; graphPane: React.ReactNode; + graphVisible?: boolean; }): React.ReactElement { const [containerWidth, setContainerWidth] = useState(() => window.innerWidth); const [layout, setLayout] = useState(() => readPaneLayout(window.localStorage)); @@ -47,6 +51,8 @@ export function WorkspacePanes({ const dragRef = useRef(); const layoutRef = useRef(layout); const pixels = clampPaneLayout(layout, containerWidth); + const showFiles = filesVisible ?? true; + const showGraph = graphVisible ?? true; useEffect(() => { const container = containerRef.current; @@ -180,14 +186,20 @@ export function WorkspacePanes({ className="workspace-grid" ref={containerRef} style={{ - gridTemplateColumns: `${pixels.files}px ${PANE_SEPARATOR_WIDTH}px ${pixels.editor}px ${PANE_SEPARATOR_WIDTH}px minmax(${MIN_GRAPH_PANE}px, 1fr)`, + gridTemplateColumns: showFiles && showGraph + ? `${pixels.files}px ${PANE_SEPARATOR_WIDTH}px ${pixels.editor}px ${PANE_SEPARATOR_WIDTH}px minmax(${MIN_GRAPH_PANE}px, 1fr)` + : showFiles + ? `${pixels.files}px ${PANE_SEPARATOR_WIDTH}px minmax(${MIN_EDITOR_PANE}px, 1fr)` + : showGraph + ? `minmax(${MIN_EDITOR_PANE}px, 1fr) ${PANE_SEPARATOR_WIDTH}px ${pixels.graph}px` + : 'minmax(0, 1fr)', }} > - {filesPane} - {separator('files', 'Resize File hierarchy and editor panes')} + {showFiles ? filesPane : null} + {showFiles ? separator('files', 'Resize File hierarchy and editor panes') : null} {editorPane} - {separator('graph', 'Resize editor and Relationship Graph panes')} - {graphPane} + {showGraph ? separator('graph', 'Resize editor and Relationship Graph panes') : null} + {showGraph ? graphPane : null} ); } diff --git a/apps/desktop/src/components/createCodeEditor.ts b/apps/desktop/src/components/createCodeEditor.ts index 7d4700a97..75f983b06 100644 --- a/apps/desktop/src/components/createCodeEditor.ts +++ b/apps/desktop/src/components/createCodeEditor.ts @@ -1,19 +1,22 @@ +import { loadEditorLanguage } from './editorLanguage'; + +export interface CreateCodeEditorOptions { + content: string; + onChange: (content: string) => void; + onSave: () => void; + parent: HTMLElement; + path: string; +} + export async function createCodeEditor({ content, onChange, onSave, parent, path, -}: { - content: string; - onChange: (content: string) => void; - onSave: () => void; - parent: HTMLElement; - path: string; -}): Promise<() => void> { +}: CreateCodeEditorOptions): Promise<() => void> { const [ { defaultKeymap, history, historyKeymap, indentWithTab }, - { javascript }, { bracketMatching, indentOnInput, @@ -31,21 +34,16 @@ export async function createCodeEditor({ keymap, lineNumbers, }, + language, ] = await Promise.all([ import('@codemirror/commands'), - import('@codemirror/lang-javascript'), import('@codemirror/language'), import('@codemirror/search'), import('@codemirror/state'), import('@codemirror/theme-one-dark'), import('@codemirror/view'), + loadEditorLanguage(path), ]); - const language = /\.[cm]?[jt]sx?$/iu.test(path) - ? javascript({ - jsx: /x$/iu.test(path), - typescript: /\.[cm]?tsx?$/iu.test(path), - }) - : []; const view = new EditorView({ parent, state: EditorState.create({ @@ -62,7 +60,7 @@ export async function createCodeEditor({ syntaxHighlighting(oneDarkHighlightStyle), highlightSelectionMatches(), highlightActiveLine(), - language, + language ?? [], keymap.of([ { key: 'Mod-s', run: () => { onSave(); return true; } }, indentWithTab, diff --git a/apps/desktop/src/components/editorLanguage.test.ts b/apps/desktop/src/components/editorLanguage.test.ts new file mode 100644 index 000000000..ff50de399 --- /dev/null +++ b/apps/desktop/src/components/editorLanguage.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { findEditorLanguage, loadEditorLanguage } from './editorLanguage'; +import { isMarkdownPath } from './markdownPath'; + +describe('desktop editor languages', () => { + it.each([ + ['src/app.ts', 'TypeScript'], + ['src/App.tsx', 'TSX'], + ['tools/check.py', 'Python'], + ['src/lib.rs', 'Rust'], + ['native/main.c', 'C'], + ['native/graph.cpp', 'C++'], + ['cmd/codegraphy/main.go', 'Go'], + ['fixtures/graph.json', 'JSON'], + ['config/site.yaml', 'YAML'], + ['public/index.html', 'HTML'], + ['styles/theme.css', 'CSS'], + ['containers/Dockerfile', 'Dockerfile'], + ['native/CMakeLists.txt', 'CMake'], + ['README.md', 'Markdown'], + ['docs/guide.mdx', 'Markdown'], + ])('matches %s to the maintained %s language support', async (path, expectedName) => { + expect(findEditorLanguage(path)?.name).toBe(expectedName); + expect(await loadEditorLanguage(path)).toBeDefined(); + }); + + it('keeps Markdown presentation limited to Markdown Files', () => { + expect(isMarkdownPath('README.md')).toBe(true); + expect(isMarkdownPath('docs/guide.mdx')).toBe(true); + expect(isMarkdownPath('docs/history.markdown')).toBe(true); + expect(isMarkdownPath('src/markdown.ts')).toBe(false); + }); +}); diff --git a/apps/desktop/src/components/editorLanguage.ts b/apps/desktop/src/components/editorLanguage.ts new file mode 100644 index 000000000..b900f773a --- /dev/null +++ b/apps/desktop/src/components/editorLanguage.ts @@ -0,0 +1,15 @@ +import { LanguageDescription, type LanguageSupport } from '@codemirror/language'; +import { languages } from '@codemirror/language-data'; +import { isMarkdownPath } from './markdownPath'; + +const markdownDescription = LanguageDescription.matchLanguageName(languages, 'Markdown'); + +export function findEditorLanguage(path: string): LanguageDescription | undefined { + const filename = path.split(/[\\/]/).at(-1) ?? path; + return LanguageDescription.matchFilename(languages, filename) + ?? (isMarkdownPath(path) ? markdownDescription ?? undefined : undefined); +} + +export async function loadEditorLanguage(path: string): Promise { + return findEditorLanguage(path)?.load(); +} diff --git a/apps/desktop/src/components/markdownPath.ts b/apps/desktop/src/components/markdownPath.ts new file mode 100644 index 000000000..7f55daa2b --- /dev/null +++ b/apps/desktop/src/components/markdownPath.ts @@ -0,0 +1,3 @@ +export function isMarkdownPath(path: string): boolean { + return /\.(?:md|markdown|mdx)$/iu.test(path); +} diff --git a/apps/desktop/src/interfacePreferences.test.ts b/apps/desktop/src/interfacePreferences.test.ts new file mode 100644 index 000000000..a20b94383 --- /dev/null +++ b/apps/desktop/src/interfacePreferences.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + DEFAULT_DESKTOP_INTERFACE_PREFERENCES, + DESKTOP_INTERFACE_PREFERENCES_KEY, + parseDesktopInterfacePreferences, + readDesktopInterfacePreferences, + saveDesktopInterfacePreferences, +} from './interfacePreferences'; + +describe('desktop interface preferences', () => { + it('accepts only the complete pane and profiling record', () => { + const preferences = { + filesPaneVisible: false, + graphPaneVisible: true, + profilingVisible: true, + }; + + expect(parseDesktopInterfacePreferences(preferences)).toEqual(preferences); + expect(parseDesktopInterfacePreferences({ graphPaneVisible: false })).toEqual( + DEFAULT_DESKTOP_INTERFACE_PREFERENCES, + ); + expect(parseDesktopInterfacePreferences({ ...preferences, extra: true })).toEqual( + DEFAULT_DESKTOP_INTERFACE_PREFERENCES, + ); + }); + + it('persists one local interface record and rejects invalid storage', () => { + const setItem = vi.fn(); + const preferences = { + filesPaneVisible: true, + graphPaneVisible: false, + profilingVisible: true, + }; + + saveDesktopInterfacePreferences({ setItem }, preferences); + + expect(setItem).toHaveBeenCalledWith( + DESKTOP_INTERFACE_PREFERENCES_KEY, + JSON.stringify(preferences), + ); + expect(readDesktopInterfacePreferences({ getItem: () => JSON.stringify(preferences) })) + .toEqual(preferences); + expect(readDesktopInterfacePreferences({ getItem: () => '{broken' })) + .toEqual(DEFAULT_DESKTOP_INTERFACE_PREFERENCES); + }); +}); diff --git a/apps/desktop/src/interfacePreferences.ts b/apps/desktop/src/interfacePreferences.ts new file mode 100644 index 000000000..24a59fe4e --- /dev/null +++ b/apps/desktop/src/interfacePreferences.ts @@ -0,0 +1,50 @@ +export interface DesktopInterfacePreferences { + filesPaneVisible: boolean; + graphPaneVisible: boolean; + profilingVisible: boolean; +} + +export const DEFAULT_DESKTOP_INTERFACE_PREFERENCES: DesktopInterfacePreferences = { + filesPaneVisible: true, + graphPaneVisible: true, + profilingVisible: false, +}; +export const DESKTOP_INTERFACE_PREFERENCES_KEY = 'codegraphy.desktop.interface-preferences'; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function parseDesktopInterfacePreferences(value: unknown): DesktopInterfacePreferences { + if (!isRecord(value) + || Object.keys(value).sort().join(',') !== 'filesPaneVisible,graphPaneVisible,profilingVisible' + || typeof value.filesPaneVisible !== 'boolean' + || typeof value.graphPaneVisible !== 'boolean' + || typeof value.profilingVisible !== 'boolean') { + return { ...DEFAULT_DESKTOP_INTERFACE_PREFERENCES }; + } + return { + filesPaneVisible: value.filesPaneVisible, + graphPaneVisible: value.graphPaneVisible, + profilingVisible: value.profilingVisible, + }; +} + +export function readDesktopInterfacePreferences( + storage: Pick, +): DesktopInterfacePreferences { + const stored = storage.getItem(DESKTOP_INTERFACE_PREFERENCES_KEY); + if (stored === null) return { ...DEFAULT_DESKTOP_INTERFACE_PREFERENCES }; + try { + return parseDesktopInterfacePreferences(JSON.parse(stored)); + } catch { + return { ...DEFAULT_DESKTOP_INTERFACE_PREFERENCES }; + } +} + +export function saveDesktopInterfacePreferences( + storage: Pick, + preferences: DesktopInterfacePreferences, +): void { + storage.setItem(DESKTOP_INTERFACE_PREFERENCES_KEY, JSON.stringify(preferences)); +} diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 90a966cef..7ed18958d 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -124,6 +124,8 @@ button:focus-visible, padding-right: 10px; } +.toolbar-actions .toolbar-button { padding-inline: 8px; } + .toolbar-button, .save-button { min-height: 28px; diff --git a/apps/desktop/src/useDesktopGraphRenderer.ts b/apps/desktop/src/useDesktopGraphRenderer.ts index 8f5c38e90..c48edfc85 100644 --- a/apps/desktop/src/useDesktopGraphRenderer.ts +++ b/apps/desktop/src/useDesktopGraphRenderer.ts @@ -68,6 +68,27 @@ interface DesktopGraphViewportControls { zoomBy(factor: number): void; } +interface CameraTransition { + from: GraphRendererCamera; + startedAt?: number; + to: GraphRendererCamera; +} + +const CAMERA_TRANSITION_DURATION_MS = 160; + +function interpolateCamera( + from: GraphRendererCamera, + to: GraphRendererCamera, + progress: number, +): GraphRendererCamera { + const eased = 1 - (1 - progress) ** 3; + return { + centerX: from.centerX + (to.centerX - from.centerX) * eased, + centerY: from.centerY + (to.centerY - from.centerY) * eased, + zoom: from.zoom + (to.zoom - from.zoom) * eased, + }; +} + const GRAPH_APPEARANCE_DEFAULTS: DesktopGraphAppearance = { labelForeground: 'CanvasText', labelMutedForeground: 'GrayText', @@ -291,6 +312,7 @@ export function useDesktopGraphRenderer({ let styleVersion = 0; let visibleFrameRequested = true; let camera: GraphRendererCamera = { centerX: 0, centerY: 0, zoom: 1 }; + let cameraTransition: CameraTransition | undefined; let pointer: PointerState | undefined; const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; const forcedColors = matchMedia('(forced-colors: active)').matches; @@ -360,9 +382,9 @@ export function useDesktopGraphRenderer({ const schedule = (): void => { if (!active || framePending) return; framePending = true; - animationFrame = requestAnimationFrame(() => { + animationFrame = requestAnimationFrame((timestamp) => { framePending = false; - draw(); + draw(timestamp); }); }; const scheduleVisible = (): void => { @@ -472,7 +494,7 @@ export function useDesktopGraphRenderer({ }); drawOverlay(); }; - const draw = (): void => { + const draw = (timestamp: number): void => { if (!renderer?.canRender() || !active) return; const tick = layout.tick(); if (tick.steps > 0) positionVersion += 1; @@ -486,6 +508,17 @@ export function useDesktopGraphRenderer({ camera = fitCamera(layout.x, layout.y, canvas.clientWidth, canvas.clientHeight); fitFrames -= 1; } + let cameraAnimating = false; + if (cameraTransition) { + cameraTransition.startedAt ??= timestamp; + const progress = Math.min( + 1, + Math.max(0, (timestamp - cameraTransition.startedAt) / CAMERA_TRANSITION_DURATION_MS), + ); + camera = interpolateCamera(cameraTransition.from, cameraTransition.to, progress); + cameraAnimating = progress < 1; + if (!cameraAnimating) cameraTransition = undefined; + } if (reduceMotion && !tick.settled) { if (visibleFrameRequested) { visibleFrameRequested = false; @@ -496,23 +529,31 @@ export function useDesktopGraphRenderer({ } return; } - continueAfterFrame = !tick.settled; + continueAfterFrame = !tick.settled || cameraAnimating; visibleFrameRequested = false; renderFrame(); + if (cameraAnimating) schedule(); }; const restyle = (): void => { styleVersion += 1; scheduleVisible(); }; - const zoomBy = (factor: number): void => { + const transitionCamera = (nextCamera: GraphRendererCamera): void => { fitFrames = 0; - camera = zoomDesktopGraphBy(camera, factor); + if (reduceMotion) { + cameraTransition = undefined; + camera = nextCamera; + } else { + cameraTransition = { from: camera, to: nextCamera }; + } scheduleVisible(); }; + const zoomBy = (factor: number): void => { + const base = cameraTransition?.to ?? camera; + transitionCamera(zoomDesktopGraphBy(base, factor)); + }; const fitToGraph = (): void => { - fitFrames = 0; - camera = fitCamera(layout.x, layout.y, canvas.clientWidth, canvas.clientHeight); - scheduleVisible(); + transitionCamera(fitCamera(layout.x, layout.y, canvas.clientWidth, canvas.clientHeight)); }; redrawRef.current = restyle; viewportControlsRef.current = { fitToScreen: fitToGraph, zoomBy }; @@ -526,6 +567,7 @@ export function useDesktopGraphRenderer({ const onWheel = (event: WheelEvent): void => { event.preventDefault(); fitFrames = 0; + cameraTransition = undefined; const bounds = canvas.getBoundingClientRect(); camera = zoomDesktopGraphAtPointer( camera, @@ -539,6 +581,7 @@ export function useDesktopGraphRenderer({ }; const onPointerDown = (event: PointerEvent): void => { if (event.button !== 0) return; + cameraTransition = undefined; const bounds = canvas.getBoundingClientRect(); const world = screenToDesktopGraph( camera, @@ -659,6 +702,7 @@ export function useDesktopGraphRenderer({ }; const onKeyDown = (event: KeyboardEvent): void => { const panDistance = 40 / camera.zoom; + if (event.key.startsWith('Arrow')) cameraTransition = undefined; if (event.key === 'ArrowLeft') camera.centerX -= panDistance; else if (event.key === 'ArrowRight') camera.centerX += panDistance; else if (event.key === 'ArrowUp') camera.centerY -= panDistance; diff --git a/docs/research/desktop-app-product-expectations.md b/docs/research/desktop-app-product-expectations.md deleted file mode 100644 index 20f287e21..000000000 --- a/docs/research/desktop-app-product-expectations.md +++ /dev/null @@ -1,183 +0,0 @@ -# Desktop app product expectations - -Research checked: 2026-08-06. This note applies current macOS, Tauri 2, and first-party editor guidance to the desktop app in `apps/desktop`. It supplements the existing [stack decision](./desktop-app-stack.md). It does not change the product model in [`CONTEXT.md`](../../CONTEXT.md). - -## Recommendation - -Keep CodeGraphy Desktop a fast, local Relationship Graph navigator with lightweight editing. Do not turn it into a general-purpose IDE. The public MVP should feel like a complete Mac app in the smaller job it chooses to do: open and switch workspaces, find a File, inspect Relationships, make a safe edit, recover from interruption, and explain failures. - -The current branch already has the right base: - -- Tauri owns the window, folder picker, validated File access, and Core child process. -- Core owns Indexing and the Graph Cache. -- The webview owns the File hierarchy, CodeMirror editor, and Relationship Graph. -- Saves use atomic replacement, preserve permissions, reject external-change conflicts, and trigger bounded one-File Indexing. -- The bundle has a narrow content security policy, a fixed sidecar, a macOS 26 floor, and a detailed signed-release acceptance gate. - -This branch also adds recent workspace navigation, native File menus, stale-request guards, the shared Material File and Folder visuals, reduced-motion rendering, direct Node drag physics, and a focused Graph Settings surface. The largest remaining product systems are persistent recovery and session state, keyboard-first find and commands, local diagnostics, updates, and a complete accessibility path. Those belong in the host and app shell, not in Core or the renderer. - -## Must-have MVP polish - -### Workspace switching and Mac conventions - -Ship one active workspace per window first. Put the workspace name in a toolbar pop-up that contains recent workspaces, `Open Workspace...`, and `Clear Recent Workspaces`. Switching replaces the current workspace in place. The welcome view should show the same recent list. Missing or moved folders should be removed after one failed open, with a plain error and a new folder picker. - -Mirror these actions in the native menu bar: - -- `File > Open Workspace...` with `Command-O` -- `File > Open Recent` with recent folders and `Clear Menu` -- `File > Close Workspace` -- standard `Save`, `Undo`, `Redo`, `Cut`, `Copy`, `Paste`, `Find`, `Window`, and `Help` items - -macOS users expect New and Open actions in the File menu, and Apple recommends familiar menu commands and shortcuts for files. AppKit also has a system recent-document model with add, list, maximum-count, and clear operations. A custom toolbar control should therefore supplement the menu, not replace it ([Apple file management](https://developer.apple.com/design/human-interface-guidelines/file-management), [Apple `recentDocumentURLs`](https://developer.apple.com/documentation/appkit/nsdocumentcontroller/recentdocumenturls)). Zed makes `File > Open`, `File > Open Recent`, project-local context, and explicit new-window behavior part of its project model ([Zed windows and projects](https://zed.dev/docs/windows-and-projects)). - -Store recent paths and the last active path as app-global UI state under the normal Application Support location. Keep `.codegraphy/graph.sqlite` and workspace settings in the workspace. Do not create a second graph or settings model in app-global storage. - -Before any workspace switch, resolve a dirty editor with `Save`, `Don't Save`, or `Cancel`. `Cancel` must leave the current workspace untouched. Stop the old workspace engine and ignore all late responses before showing the next workspace. This prevents a rapid switch from painting an old File or graph into the new workspace. - -### Fast navigation and search - -Add a small command registry that drives both native menu items and a command palette. The minimum commands are open or switch workspace, open recent workspace, quick-open File, search workspace text, save, close File, reveal File in Finder, re-index, focus each pane, show settings, show logs, and check for updates. - -Use the conventions people already know: - -- `Command-P`: fuzzy File finder, ordered by match quality and recency -- `Shift-Command-P`: command palette -- `Command-F`: search in the open File -- `Shift-Command-F`: search text across the workspace -- `Command-S`: save -- `Command-,`: settings -- `Control-Tab`: move through recent Files - -VS Code and Zed both treat quick File open, workspace search, recent-file navigation, and a command palette as primary navigation, not advanced features ([VS Code user interface](https://code.visualstudio.com/docs/editing/userinterface), [Zed finding and navigating](https://zed.dev/docs/finding-navigating)). Obsidian reaches the same result for local folders: its quick switcher is keyboard-only, shows recent Files when the query is empty, and changes its search algorithm above 10,000 items to keep it responsive ([Obsidian quick switcher](https://obsidian.md/help/plugins/quick-switcher)). - -The File hierarchy must support arrow-key navigation, left and right to collapse or expand folders, Return to open, type-ahead selection, and automatic reveal of the active File. Zed documents the same project-tree behavior and also provides reveal in Finder and directory-scoped search ([Zed project panel](https://zed.dev/docs/project-panel)). - -Do not make the first command palette a plugin platform. A typed list of shell-owned commands is enough. It should use fuzzy matching, display shortcuts, keep the last few commands near the top, and remain fully usable with the keyboard. Obsidian and Zed both expose all commands this way ([Obsidian command palette](https://obsidian.md/help/plugins/command-palette), [Zed command palette](https://zed.dev/docs/command-palette)). - -### Safe editing, recovery, and conflicts - -Keep explicit source saves as the default for the first release. Code editors often need a deliberate save boundary because saving can run watchers, builds, and CodeGraphy incremental Indexing. Zed also defaults autosave to off while offering focus-change and window-change modes ([Zed settings](https://zed.dev/docs/reference/all-settings#autosave)). - -Explicit save must not mean data loss. Persist a local recovery snapshot of each dirty buffer after a short idle interval and before workspace switch, window close, update relaunch, or app quit. Recovery data belongs in Application Support, outside the workspace. Key it by canonical workspace path, relative File path, and base revision. On relaunch, restore the unsaved buffer and label it as recovered. Delete its recovery entry only after a successful save or an explicit discard. - -This follows the useful part of VS Code Hot Exit, which restores unsaved changes after quit, and Obsidian File Recovery, which keeps local snapshots outside the workspace ([VS Code Hot Exit](https://code.visualstudio.com/docs/editing/codebasics#_hot-exit), [Obsidian File Recovery](https://obsidian.md/help/plugins/file-recovery)). Apple recommends periodic preservation while editing and when a file closes or the app loses focus ([Apple file management](https://developer.apple.com/design/human-interface-guidelines/file-management#Saving-work)). - -Keep the existing revision check and atomic replacement. Improve the conflict UI so a rejected save keeps the user's draft and offers: - -- `Compare` against the current disk contents -- `Reload From Disk`, with confirmation because it discards the draft -- `Keep Editing`, with copy-to-clipboard available - -Never turn an external-change conflict into a status-bar-only error. If autosave is added later, suspend it while a conflict is unresolved. AppKit's document model treats conflict resolution as a first-class document event rather than an overwrite fallback ([Apple `NSDocument`](https://developer.apple.com/documentation/appkit/nsdocument)). - -### Session and window restore - -Restore the last workspace, selected File, sidebar expansion, pane widths, graph camera, and window position after a normal quit. Restore only identifiers and view state, never a copied source File or graph. If the workspace no longer exists, open the recent-workspace welcome view. If recovery data exists, restore the dirty editor from that data. - -Use Tauri's maintained window-state plugin for size and position, and keep the remaining session record in app-global storage ([Tauri window state](https://v2.tauri.app/plugin/window-state/), [Tauri store](https://v2.tauri.app/plugin/store/)). Apple's AppKit example restores window geometry, selection, tabs, and in-progress editor state to preserve continuity ([Apple AppKit state restoration](https://developer.apple.com/documentation/appkit/restoring-your-app-s-state-with-appkit)). Zed also restores workspaces on startup and persists tabs, panes, scroll positions, and recent projects in local app data ([Zed settings](https://zed.dev/docs/reference/all-settings#restore-on-startup), [Zed troubleshooting](https://zed.dev/docs/troubleshooting#startup-and-workspace-issues)). - -### Settings and appearance - -Add a small searchable settings view opened with `Command-,`. For MVP, include appearance, editor font and size, tab size, line wrapping, autosave choice, restore-on-startup choice, reduced graph motion, and update preference. Continue to use `.codegraphy/settings.json` for shared workspace behavior. Store Mac UI preferences globally. Show clearly which level owns each setting. - -Support system light and dark appearance by default, plus CodeGraphy's own theme and File icon theme. Reuse the extension's Material-style File and Folder colors and icons so the same Node means the same thing in both interfaces. Do not encode File type or selection with color alone. VS Code separates user and workspace settings and gives settings a searchable editor; Zed exposes theme, icon theme, UI font, and editor font controls ([VS Code user interface](https://code.visualstudio.com/docs/editing/userinterface#_settings), [Zed appearance](https://zed.dev/docs/appearance)). - -### Accessibility - -Treat accessibility as a release gate: - -- Give every control an accessible name and visible keyboard focus. -- Implement the File hierarchy as a semantic tree, not a list of clickable text. -- Expose selected, expanded, busy, dirty, error, and disabled states to assistive technology. -- Provide a keyboard-accessible list or inspector for graph Nodes and Relationships. An `aria-label` on the canvas alone does not expose its content. -- Keep text and essential icons at WCAG AA contrast in light and dark themes. Use shape, outline, or text in addition to color. -- Respect Reduce Motion. Stop continuous decorative graph motion when requested, and provide a pause control in all modes. -- Verify VoiceOver, Full Keyboard Access, zoom, increased contrast, and reduced motion in the packaged app. - -Apple asks apps to support keyboard-only use, VoiceOver labels, sufficient contrast, and reduced automatic motion. Its guidance uses 4.5:1 as the minimum contrast for normal text and 3:1 for large or bold text ([Apple keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards), [Apple accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility)). VS Code also treats zoom, high-contrast colors, keyboard-only navigation, and screen-reader behavior as one accessibility system ([VS Code accessibility](https://code.visualstudio.com/docs/configure/accessibility/accessibility)). - -### Local diagnostics and failure recovery - -Add structured local logs for app startup, workspace open and close, Core spawn and exit, Graph Cache state, Indexing duration and counts, File read and save outcomes, renderer initialization, first frame, and update checks. Capture sidecar stderr in the same session log. Rotate logs by size and retain a small bounded history under `~/Library/Logs/dev.codegraphy.desktop`; Tauri's log plugin uses the platform log directory and supports file rotation ([Tauri logging](https://v2.tauri.app/plugin/logging/)). - -Never log source contents. Treat absolute paths as private by default. Prefer workspace-relative paths, counts, durations, versions, error categories, and a per-session identifier. Apple explicitly warns against privacy-sensitive log data and recommends keeping symbol information and release archives so crash reports can be symbolicated ([Apple logging](https://developer.apple.com/documentation/os/generating-log-messages-from-your-code), [Apple crash reports](https://developer.apple.com/documentation/xcode/diagnosing-issues-using-crash-reports-and-device-logs)). - -Add `Help > Show Logs in Finder` and `Help > Copy Diagnostic Summary`. The summary should contain app, macOS, WebKit, Core, and bundled Node versions; architecture; renderer capability; Graph Cache status; last Core exit; and log location. It must not contain source text. A Core crash should keep the window alive, explain what stopped, and offer `Restart Core` and `Re-index` rather than leaving an orphan process or a frozen busy state. - -### Security - -Keep the webview untrusted and the host API narrow: - -- Continue to canonicalize the chosen root and reject absolute paths, traversal, and symlinks that leave it. -- Keep File access in typed Rust commands scoped to the active workspace. Do not add broad frontend filesystem permissions. -- Keep the sidecar executable and arguments fixed. Do not expose arbitrary shell execution. -- Grant Tauri capabilities per window and command. Avoid overlapping capability files because Tauri merges their permissions. -- Keep a restrictive content security policy. Do not load remote scripts or give remote content IPC access. -- If workspace or interface plugins can execute code, add a workspace trust decision before activation. Browsing Files and a cached graph should remain available in restricted mode. -- Audit every Hardened Runtime exception. Keep only exceptions proven necessary by the packaged renderer and sidecar. - -Tauri's capabilities bind permissions to specific windows and webviews, while command scopes narrow allowed resources. Broad capabilities increase the effect of a frontend compromise ([Tauri capabilities](https://v2.tauri.app/security/capabilities/), [Tauri permissions](https://v2.tauri.app/security/permissions/)). Tauri's filesystem guidance also rejects traversal and gives deny scopes precedence over allow scopes ([Tauri filesystem security](https://v2.tauri.app/plugin/file-system/#security)). VS Code uses restricted mode because workspace configuration and extensions can execute code; CodeGraphy needs the same boundary once opening a workspace can activate executable plugins ([VS Code Workspace Trust](https://code.visualstudio.com/docs/editing/workspaces/workspace-trust)). - -### Performance contract - -Make responsiveness measurable in the release app. Record these proposed product budgets on supported Apple Silicon hardware, then adjust them only from measurements: - -- show the restored window shell within 500 ms of process launch -- show a cached File hierarchy and graph within 1 second of workspace selection -- show a recently opened File within 100 ms and an uncached small text File within 200 ms -- keep typing and graph interaction at the display refresh rate with no long main-thread task above 50 ms -- change workspaces without a stale File, graph, status, or Core response appearing - -Use request generations or cancellation for File and workspace loads, an in-memory most-recently-used buffer cache, a virtualized File tree for large workspaces, and progressive graph presentation from the Graph Cache. Keep full Indexing off the critical open path. Show progress and allow cancellation when explicit re-indexing takes longer than a brief interaction. - -The numbers above are CodeGraphy budgets, not claims from the cited peers. The peer pattern is clear: keyboard File finders, cached per-project state, and bounded behavior for large workspaces. Obsidian changes its quick-switch algorithm above 10,000 items, and Zed stores project state locally for fast restoration ([Obsidian quick switcher](https://obsidian.md/help/plugins/quick-switcher), [Zed troubleshooting](https://zed.dev/docs/troubleshooting#startup-and-workspace-issues)). Add macOS signposts around startup, workspace switch, File open, Core requests, and first graph frame so Instruments can verify the budgets; Apple's logging system supports time-based signposts for performance analysis ([Apple logging](https://developer.apple.com/documentation/os/logging)). - -### Updates, packaging, and distribution - -Do not enable in-app updates until the first Developer ID signed, notarized, stapled, installed, and runtime-tested release exists. After that gate passes: - -- check in the background after launch and through `CodeGraphy > Check for Updates...` -- show version, notes, download progress, and a clear relaunch action -- save or recover every dirty buffer before relaunch -- use HTTPS endpoints and Tauri's separate updater signing key -- keep the updater private key separate from Apple signing credentials -- provide a preference for automatic checks, not a hidden mandatory update - -Tauri requires cryptographic update signatures and does not allow verification to be disabled. Its static JSON format supports platform-specific URLs and signatures, including GitHub Release assets ([Tauri updater](https://v2.tauri.app/plugin/updater/)). - -Keep the existing Apple Silicon DMG path and installed-app acceptance test. Direct distribution requires Developer ID signing, Hardened Runtime, notarization, and a stapled ticket. Apple also requires all nested executable code to have valid signatures and warns against `get-task-allow` in distribution builds ([Apple notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution), [Apple notarization issues](https://developer.apple.com/documentation/security/resolving-common-notarization-issues)). Tauri supports direct-download DMGs and states that direct macOS distribution requires signing and notarization ([Tauri distribution](https://v2.tauri.app/distribute/), [Tauri macOS signing](https://v2.tauri.app/distribute/sign/macos/)). - -The release artifact should also include a standard About panel with version and copyright, an application icon at all required sizes, release notes, a privacy statement that explains local data and diagnostics, and a support path. Preserve dSYMs and exact release archives for every published version. - -## Follow-up features - -These are useful, but they should follow a solid single-workspace MVP: - -- multiple windows and explicit `Open in New Window` -- Finder `Open With`, folder drag-and-drop, and single-instance routing to the existing window -- multi-root workspaces -- configurable autosave on focus or window change -- recovered-buffer history and a visual diff beyond the latest conflict -- editable keyboard shortcuts and user-defined command aliases -- saved searches, replace across Files, and search history -- Graph Scope, Filters, plugin controls, and Symbol navigation after the File and Folder-only path is polished -- extension or plugin marketplace behavior -- Intel builds, only after the same renderer, Core, signing, and installed-app probes pass on Intel hardware -- opt-in crash upload or analytics, with payload preview and source-content exclusion -- Mac App Store distribution and App Sandbox bookmarks, only if store distribution becomes a product requirement - -Fleet is the caution here. JetBrains separated editor, workspace, and system-service responsibilities, but later stopped Fleet distribution after it overlapped its established IDEs without a strong enough reason to switch ([Fleet architecture](https://blog.jetbrains.com/fleet/2022/01/fleet-below-deck-part-i-architecture-overview/), [The future of Fleet](https://blog.jetbrains.com/fleet/2025/12/the-future-of-fleet/)). CodeGraphy's reason to exist is the fast, explainable Relationship Graph. Every follow-up feature should make that navigation loop safer or faster. - -Lapce shows the other edge of the scope decision. Its official project includes a native Rust and `wgpu` editor, LSP, remote development, plugins, and a terminal ([Lapce repository](https://github.com/lapce/lapce)). Those are credible editor features, but adopting them would move CodeGraphy away from its focused navigation job. - -## Suggested delivery order - -1. Quick File open, keyboard tree navigation, and workspace text search. -2. Recovery snapshots, conflict actions, and normal-quit session restoration. -3. Searchable app and editor settings beyond the current focused Graph Settings surface. -4. Accessibility pass, including a keyboard-accessible Node list and Graph Stage pause control. -5. Structured local logs, Core restart, diagnostic summary, and measured performance budgets. -6. Signed first release. Add the signed updater only after that release path passes on a clean Mac. - -This order keeps each layer usable before the next one arrives and keeps Core and the renderer free of desktop product state. diff --git a/docs/research/desktop-app-stack.md b/docs/research/desktop-app-stack.md deleted file mode 100644 index bfcecdc25..000000000 --- a/docs/research/desktop-app-stack.md +++ /dev/null @@ -1,100 +0,0 @@ -# macOS desktop app stack research - -Research completed: 2026-08-06. Sources were checked on that date. This note records the stack decision for Trello #275; it does not redefine the product model in [`CONTEXT.md`](../../CONTEXT.md) or the principles in [`docs/PHILOSOPHY.md`](../PHILOSOPHY.md). - -## Decision - -Build the macOS MVP as a **Tauri 2 app with a Rust shell, the existing React/Vite web stack, CodeMirror 6, the existing WebGPU/WASM graph renderer, and a bundled Core service process**. - -Set **macOS 26.0 as the minimum system version**. This is a product constraint, not a build default: Tauri uses the operating system's `WKWebView`, and WebKit shipped WebGPU in Safari 26 on macOS 26. CodeGraphy's graph renderer has no non-WebGPU implementation. Supporting macOS 25 or earlier would therefore require either a second renderer or a bundled Chromium runtime. Both conflict with the smallest long-term architecture for this MVP. Tauri exposes `bundle.macOS.minimumSystemVersion` for enforcing this requirement in the app bundle ([Tauri webview versions](https://v2.tauri.app/reference/webview-versions/), updated 2026; [WebKit, “News from WWDC25,” June 9, 2025](https://webkit.org/blog/16993/news-from-wwdc25-web-technology-coming-this-fall-in-safari-26-beta/); [Tauri macOS app bundle](https://v2.tauri.app/distribute/macos-application-bundle/)). - -Ship native Apple Silicon and Intel artifacts only after each architecture passes the same runtime probe. Apple Silicon is the first required artifact. Do not label an Intel artifact supported until `navigator.gpu`, adapter creation, the CodeGraphy render pipeline, the bundled native Core modules, signing, notarization, and an installed-app launch all pass on Intel hardware. - -## Architecture - -```text -CodeGraphy.app -├── Tauri 2 Rust shell -│ ├── macOS window, menu, open dialog, and lifecycle -│ ├── narrow, validated file read/save commands -│ └── owns and monitors the Core child process -├── WKWebView frontend -│ ├── thin File and Folder hierarchy -│ ├── CodeMirror 6 file viewer/editor -│ └── existing @codegraphy-dev/graph-renderer WebGPU/WASM Graph View -└── bundled Core service process - ├── existing @codegraphy-dev/core and native dependencies - ├── File Discovery, Tree-sitter Analysis, and Indexing - ├── workspace-local .codegraphy/graph.sqlite Graph Cache - └── request/response plus lifecycle events over JSON Lines stdio -``` - -The ownership rules are: - -- **Core remains the only graph engine.** The desktop app must call Core for File Discovery, Indexing, Graph Cache reads and writes, Nodes, Symbols, Relationships, Graph Scope, Filters, and plugin analysis. Rust must not recreate these systems. -- **Rust owns host integration, not product analysis.** It opens native dialogs, validates workspace-relative paths, performs safe file reads and writes, supervises the child process, and translates a small typed command/event protocol for the webview. -- **The webview owns presentation.** It renders the hierarchy, editor, and Graph View. It does not get unrestricted filesystem or shell access. -- **Use one long-running Core process for the active CodeGraphy Workspace.** Communicate through newline-delimited JSON on stdin/stdout with request IDs and explicit lifecycle events. Stderr is diagnostic output. This avoids a localhost port, an authentication scheme, duplicate CLI launches, and repeated loading of Tree-sitter and SQLite native modules. -- **Bundle a matching Node runtime and the built Core package as a Tauri sidecar resource for each CPU architecture.** Tauri explicitly supports external binaries with target-triple names and process control ([Tauri external binaries](https://v2.tauri.app/develop/sidecar/)). Do not require a user's Node or pnpm installation. Do not make Node's single-executable application feature the release foundation yet: Node 26 still marks it as active development and its current CI statement only tests macOS arm64 ([Node.js single executable applications, v26.5.0](https://nodejs.org/api/single-executable-applications.html)). A conventional bundled runtime is larger but is the reliable path for Core's native Tree-sitter and `libsql` dependencies. -- **Keep workspace state user-owned.** The Graph Cache and workspace settings remain in `.codegraphy/`; app-global UI state uses the normal macOS Application Support location. Do not copy source files or graph data into an account or cloud service. - -The existing repository supports this split. `@codegraphy-dev/core` already exports a headless API and the `codegraphy` CLI, owns `.codegraphy/graph.sqlite`, and depends on native Tree-sitter grammars and `libsql`. `@codegraphy-dev/graph-renderer` already owns WebGPU drawing and WASM physics. React and Vite are already used by CodeGraphy interfaces. The desktop package should compose these boundaries instead of moving their behavior into Rust. - -## Why Tauri 2 - -Tauri 2 has the exact responsibilities needed here: a Rust application process, the system webview, scoped frontend-to-Rust commands, sidecar bundling, macOS bundles, DMGs, signing, notarization, an updater, and a maintained GitHub release action. Its documented process model uses `WKWebView` on macOS ([Tauri process model](https://v2.tauri.app/concept/process-model/)). Its sidecar mechanism accepts executables written in any language and bundles per-target binaries ([Tauri external binaries](https://v2.tauri.app/develop/sidecar/)). - -The realistic alternatives are worse for this product now: - -| Option | Benefit | Reason not selected | -|---|---|---| -| Electron | Bundled Chromium gives a controlled WebGPU version on older macOS releases, and its Node main/utility processes could load Core directly. | It does not meet the Rust-shell direction and bundles Chromium and Node into a larger multi-process runtime. Electron's own documentation says it embeds both Chromium and Node.js ([Electron introduction](https://www.electronjs.org/docs/latest/)); its process model adds a Node main process and Chromium renderer processes ([Electron process model](https://www.electronjs.org/docs/latest/tutorial/process-model)). Choose it only if supporting pre-macOS-26 systems becomes more important than the thin Rust shell. | -| Raw `wry`/`tao` | Maximum control with the same Rust and `WKWebView` foundation. | `wry` is the webview library underneath this class of app, not the complete security, command, bundling, updater, and release system ([wry README](https://github.com/tauri-apps/wry)). Rebuilding those layers adds product risk without solving the WebGPU OS floor. | -| SwiftUI/AppKit plus `WKWebView` | Best access to native macOS controls. | It still inherits the same `WKWebView` WebGPU constraint while adding a second UI language and manual Rust/Swift integration. It does not improve reuse of the existing React Graph View. | -| GPUI, Floem, or another native Rust UI | Native GPU rendering and very low overhead are possible. | This path requires replacing both the web UI and the editor integration. Zed's GPUI is custom, pre-1.0, and uses Metal on macOS ([GPUI README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md)). Floem says it is still maturing and can make breaking changes ([Floem README](https://github.com/lapce/floem)). Adopting either would turn the MVP into an editor and renderer rewrite. | - -## Why CodeMirror 6 - -Use **CodeMirror 6**, loaded with only the language and editor extensions required for the open file. The MVP needs fast viewing, basic syntax highlighting, editing, undo/redo, search, and safe save. It does not need to reproduce an IDE language-service host. - -CodeMirror is an extensible web editor with a minimal setup and separately installed language packages ([CodeMirror](https://codemirror.net/); [CodeMirror core extensions](https://codemirror.com/docs/extensions/)). Its current changelog shows active maintenance through June 2026, including accessibility and large-document fixes ([CodeMirror changelog](https://codemirror.com/docs/changelog/)). This fits a focused code navigator where Core, not the editor, already performs Tree-sitter analysis. - -Monaco is a strong engine, but it is the wrong default here. Microsoft describes Monaco as the fully featured VS Code editor, generated from VS Code source with service shims. Smart features run in web workers; models need durable URIs and disposal; VS Code extensions do not run in Monaco ([Monaco README and FAQ](https://github.com/microsoft/monaco-editor)). Those systems are valuable for an IDE, but they duplicate complexity before CodeGraphy needs it. Monaco should be reconsidered only if the product explicitly adds IDE-grade language services that CodeMirror extensions and an LSP client cannot satisfy. - -Obsidian is useful evidence for this choice: its official changelog exposes its CodeMirror 6 extension API and records Electron and CodeMirror upgrades ([Obsidian 0.13.7, December 3, 2021](https://obsidian.md/changelog/page/17/); [Obsidian 1.8.3, January 28, 2025](https://obsidian.md/changelog/page/7/)). CodeGraphy should copy the product lesson—local files, a metadata cache, a focused editor, and a graph—not Obsidian's Electron shell. - -## Lessons from comparable tools - -- **Obsidian: keep Files authoritative and the cache rebuildable.** Obsidian says a vault is a local filesystem folder of plain-text files, external tools can edit them, and its local metadata cache powers Graph and Outline views ([Obsidian storage help](https://obsidian.md/help/Files%2Band%2Bfolders/How%2BObsidian%2Bstores%2Bdata)). This closely matches CodeGraphy's workspace-owned Files and rebuildable Graph Cache. -- **Zed: native performance is real, but owning the whole stack is a different product investment.** Zed says it built a custom Rust GPU UI framework and organized the app around shaders; GPUI uses Metal on macOS ([Zed 1.0, April 29, 2026](https://zed.dev/blog/zed-1-0); [GPUI README](https://github.com/zed-industries/zed/blob/main/crates/gpui/README.md)). CodeGraphy already has a specialized WebGPU renderer, so it should reuse that advantage without also building a native editor and UI toolkit. -- **Lapce: a Rust-native editor requires dedicated editor architecture.** Lapce is pure Rust, uses Floem, Rope Science, and `wgpu`, and includes LSP and remote-development systems ([Lapce README](https://github.com/lapce/lapce)). That validates the ceiling of native Rust, but also shows the scope that CodeGraphy avoids by adopting CodeMirror. -- **Fleet: keep the product sharply differentiated.** Fleet separated frontend editor behavior, workspace state, and a Rust system daemon in a distributed architecture ([Fleet architecture overview, January 2022](https://blog.jetbrains.com/fleet/2022/01/fleet-below-deck-part-i-architecture-overview/)). JetBrains later ended Fleet distribution because a second general-purpose IDE family overlapped existing products and lacked a clear reason to switch ([The Future of Fleet, December 2025, updated May 2026](https://blog.jetbrains.com/fleet/2025/12/the-future-of-fleet/)). CodeGraphy should remain a fast Relationship Graph navigator with lightweight editing, not expand the MVP into another general-purpose IDE. - -## macOS packaging and release - -The direct-download release path is: - -1. Build architecture-specific app bundles and DMGs on macOS runners. Tauri calls DMG the common direct-download format and produces it with `tauri build --bundles dmg` ([Tauri DMG guide](https://v2.tauri.app/distribute/dmg/)). -2. Sign the app, bundled Node executable, native `.node` modules, and other nested executable code with a **Developer ID Application** certificate and Hardened Runtime. Apple requires distribution signing before direct distribution; its guidance requires a valid signature, secure timestamp, and Hardened Runtime for notarization ([Apple distribution signing](https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac); [Apple notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)). -3. Notarize the final deliverable with `notarytool` through Tauri's supported App Store Connect API-key or Apple-ID environment variables, staple the ticket, and inspect the notary log. Tauri documents these credential hooks and states that notarization is required with Developer ID Application signing ([Tauri macOS signing, updated May 17, 2026](https://v2.tauri.app/distribute/sign/macos/)). -4. Verify the installed artifact, not only the build directory: mount the DMG, copy the app to `/Applications`, launch it, run `codesign --verify --deep --strict --verbose=2`, run `spctl --assess --type execute --verbose=4`, confirm the stapled ticket, open a real workspace, save a File, index it, and render its Relationship Graph. -5. Upload only verified artifacts to a draft GitHub Release. Tauri's maintained action builds targets, can create a draft release, and uploads artifacts; its documented matrix includes separate Apple Silicon and Intel builds ([Tauri GitHub pipeline](https://v2.tauri.app/distribute/pipelines/github/); [tauri-action](https://github.com/tauri-apps/tauri-action)). The repository's release workflow should own the final trigger and version source. -6. Enable the Tauri updater only after the first signed release path works. The updater requires cryptographic signatures and does not allow signature verification to be disabled. It can consume `latest.json` from GitHub Releases, and `tauri-action` can generate that file ([Tauri updater](https://v2.tauri.app/plugin/updater/)). Keep the updater private key separate from the Apple signing identity and store both only in protected release secrets. - -An unsigned or ad-hoc-signed DMG is a development artifact, not a production download. Apple tightened Gatekeeper behavior in macOS Sequoia, and Apple recommends notarization for software distributed outside the Mac App Store ([Apple developer news, August 6, 2024](https://developer.apple.com/news/?id=saqachfa)). The website download control must remain visibly gated until a signed, notarized, stapled, installed, and runtime-tested GitHub Release asset exists. - -## Required proof before release - -- A startup probe records the macOS and WebKit versions, `navigator.gpu` availability, adapter/device creation, and the first successful renderer frame. -- The bundled Core process reports its Core and Node versions and successfully loads every native Tree-sitter grammar plus `libsql` on the release architecture. -- Killing the window or changing workspaces terminates the old Core process. A crash produces a visible recoverable error and diagnostic output rather than an orphan process. -- File save uses a workspace-relative validated path and detects an external modification before overwrite. A successful save triggers bounded Core Indexing and the same Core-owned Relationship Graph appears again. -- Signing verification covers nested binaries. Notarization, stapling, Gatekeeper assessment, DMG install, updater signature metadata, and the complete three-pane runtime path all run against the final release artifact. - -## Reconsideration triggers - -Revisit this decision only when a product requirement changes: - -- If CodeGraphy must support macOS 25 or earlier while retaining the current WebGPU-only renderer, switch the shell decision to Electron or fund a separately scoped embedded-Chromium/native-renderer investigation. -- If CodeGraphy becomes a general-purpose IDE with deep language-service behavior, reevaluate Monaco or a native editor architecture. -- If Core gains a stable native executable that fully owns current Node/Core behavior, replace the bundled Node service without changing the stdio protocol or UI ownership boundaries. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5247cc52..c2643ea50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 16.2.11 '@poleski/quality-tools': specifier: 0.2.1 - version: 0.2.1(@types/node@20.19.37)(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.2.1(@types/node@20.19.37)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/node': specifier: ^20.11.0 version: 20.19.37 @@ -43,7 +43,7 @@ importers: version: 1.110.0 '@vitest/coverage-istanbul': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@vscode/vsce': specifier: ^3.7.1 version: 3.7.1 @@ -94,7 +94,7 @@ importers: version: 8.57.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) apps/desktop: dependencies: @@ -110,12 +110,12 @@ importers: '@codemirror/commands': specifier: ^6.10.4 version: 6.10.4 - '@codemirror/lang-javascript': - specifier: ^6.2.5 - version: 6.2.5 '@codemirror/language': specifier: ^6.12.4 version: 6.12.4 + '@codemirror/language-data': + specifier: ^6.5.2 + version: 6.5.2 '@codemirror/search': specifier: ^6.7.1 version: 6.7.1 @@ -143,6 +143,12 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.28)(react@18.3.1) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 devDependencies: '@tauri-apps/cli': specifier: ^2.11.4 @@ -167,7 +173,7 @@ importers: version: 6.4.1(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) apps/web: dependencies: @@ -580,7 +586,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) packages/plugin-api: {} @@ -782,7 +788,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) packages: @@ -1135,12 +1141,78 @@ packages: '@codemirror/commands@6.10.4': resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.12': + resolution: {integrity: sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + '@codemirror/lang-javascript@6.2.5': resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + '@codemirror/lang-jinja@6.0.1': + resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.2': + resolution: {integrity: sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.3': + resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + '@codemirror/language@6.12.4': resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + '@codemirror/legacy-modes@6.5.3': + resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==} + '@codemirror/lint@6.9.7': resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} @@ -1931,15 +2003,54 @@ packages: '@lezer/common@1.5.2': resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + '@lezer/cpp@1.1.6': + resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==} + + '@lezer/css@1.3.6': + resolution: {integrity: sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + '@lezer/highlight@1.2.3': resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + '@lezer/javascript@1.5.4': resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + '@lezer/markdown@1.7.2': + resolution: {integrity: sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.19': + resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + '@libsql/darwin-arm64@0.5.29': resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} cpu: [arm64] @@ -3947,18 +4058,33 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -3993,6 +4119,12 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -4068,6 +4200,9 @@ packages: resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} engines: {node: '>=20.0.0'} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] @@ -4550,6 +4685,9 @@ packages: azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4660,6 +4798,9 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -4672,6 +4813,18 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -4751,6 +4904,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -4853,6 +5009,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -4929,6 +5088,9 @@ packages: devalue@5.8.1: resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -5085,6 +5247,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -5188,6 +5354,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -5227,6 +5396,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -5494,6 +5666,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -5513,6 +5691,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -5583,10 +5764,19 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -5630,6 +5820,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5663,6 +5856,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -6104,6 +6300,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6143,6 +6342,9 @@ packages: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + material-icon-theme@5.33.1: resolution: {integrity: sha512-imiILyUW/Y6YvxbobfReC4PpzjPfpMTUiiH9gN5sloQ3dJAhfJRE40l2q8pMT8+V4xRHiIZ5vaZtJw4D03qduQ==} engines: {vscode: ^1.55.0} @@ -6151,6 +6353,51 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -6161,6 +6408,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -6488,6 +6819,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} @@ -6685,6 +7019,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + prosemirror-changeset@2.4.1: resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} @@ -6790,6 +7127,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -6874,6 +7217,18 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + rename-keys@1.2.0: resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} engines: {node: '>= 0.8.0'} @@ -7089,6 +7444,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawn-command@0.0.2: resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} @@ -7166,6 +7524,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7207,6 +7568,12 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -7513,6 +7880,12 @@ packages: tree-sitter@0.25.1: resolution: {integrity: sha512-mrcEdkYtHfrK1A6fs3O6FxkBo0Qig5XUXqHhxUOQu0bmPo00QF4XaSx4edpazdHwxnSCjlGKGgIqWdaN4dvTLA==} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -7613,6 +7986,24 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -7675,6 +8066,12 @@ packages: resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} engines: {node: '>=4'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -8003,6 +8400,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -8548,6 +8948,53 @@ snapshots: '@codemirror/view': 6.43.8 '@lezer/common': 1.5.2 + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/cpp': 1.1.6 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.12': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/java': 1.1.3 + '@codemirror/lang-javascript@6.2.5': dependencies: '@codemirror/autocomplete': 6.20.3 @@ -8558,6 +9005,150 @@ snapshots: '@lezer/common': 1.5.2 '@lezer/javascript': 1.5.4 + '@codemirror/lang-jinja@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-markdown@6.5.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/markdown': 1.7.2 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/python': 1.1.19 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.3': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.2 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.3 + '@codemirror/language': 6.12.4 + '@codemirror/legacy-modes': 6.5.3 + '@codemirror/language@6.12.4': dependencies: '@codemirror/state': 6.7.1 @@ -8567,6 +9158,10 @@ snapshots: '@lezer/lr': 1.4.10 style-mod: 4.1.3 + '@codemirror/legacy-modes@6.5.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/lint@6.9.7': dependencies: '@codemirror/state': 6.7.1 @@ -9153,20 +9748,97 @@ snapshots: '@lezer/common@1.5.2': {} + '@lezer/cpp@1.1.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/css@1.3.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/highlight@1.2.3': dependencies: '@lezer/common': 1.5.2 + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/javascript@1.5.4': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/lr@1.4.10': dependencies: '@lezer/common': 1.5.2 + '@lezer/markdown@1.7.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/python@1.1.19': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@libsql/darwin-arm64@0.5.29': optional: true @@ -9276,11 +9948,11 @@ snapshots: dependencies: playwright: 1.58.2 - '@poleski/quality-tools@0.2.1(@types/node@20.19.37)(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@poleski/quality-tools@0.2.1(@types/node@20.19.37)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@stryker-mutator/api': 9.6.1 '@stryker-mutator/core': 9.6.1(@types/node@20.19.37) - '@stryker-mutator/vitest-runner': 9.6.1(@stryker-mutator/core@9.6.1(@types/node@20.19.37))(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + '@stryker-mutator/vitest-runner': 9.6.1(@stryker-mutator/core@9.6.1(@types/node@20.19.37))(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) glob: 13.0.6 semver: 7.7.4 typescript: 5.9.3 @@ -10668,14 +11340,14 @@ snapshots: '@stryker-mutator/util@9.6.1': {} - '@stryker-mutator/vitest-runner@9.6.1(@stryker-mutator/core@9.6.1(@types/node@20.19.37))(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@stryker-mutator/vitest-runner@9.6.1(@stryker-mutator/core@9.6.1(@types/node@20.19.37))(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@stryker-mutator/api': 9.6.1 '@stryker-mutator/core': 9.6.1(@types/node@20.19.37) '@stryker-mutator/util': 9.6.1 semver: 7.7.4 tslib: 2.8.1 - vitest: 3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: @@ -11214,14 +11886,32 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mocha@10.0.10': {} + '@types/ms@2.1.0': {} + '@types/node@12.20.55': {} '@types/node@20.19.37': @@ -11253,6 +11943,10 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} '@types/vscode@1.110.0': {} @@ -11364,6 +12058,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@ungap/structured-clone@1.3.3': {} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true @@ -11478,7 +12174,7 @@ snapshots: vite: 7.3.5(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.35(typescript@5.9.3) - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.3(supports-color@8.1.1) @@ -11490,7 +12186,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -11874,6 +12570,8 @@ snapshots: tunnel: 0.0.6 typed-rest-client: 1.8.11 + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -11976,6 +12674,8 @@ snapshots: caniuse-lite@1.0.30001806: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -11991,6 +12691,14 @@ snapshots: chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} check-error@2.1.3: {} @@ -12082,6 +12790,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} commander@13.1.0: {} @@ -12178,6 +12888,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -12258,6 +12972,10 @@ snapshots: devalue@5.8.1: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} diff-match-patch@1.0.5: {} @@ -12527,6 +13245,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.13.6 @@ -12666,6 +13386,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -12714,6 +13436,8 @@ snapshots: expect-type@1.3.0: {} + extend@3.0.2: {} + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -12996,6 +13720,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + he@1.2.0: {} hosted-git-info@4.1.0: @@ -13012,6 +13760,8 @@ snapshots: html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -13074,12 +13824,21 @@ snapshots: ini@1.3.8: optional: true + inline-style-parser@0.2.7: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -13131,6 +13890,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -13159,6 +13920,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -13607,6 +14370,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -13650,6 +14415,8 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-table@3.0.4: {} + material-icon-theme@5.33.1: dependencies: chroma-js: 3.2.0 @@ -13659,12 +14426,356 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdurl@2.0.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3(supports-color@8.1.1) + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -13995,6 +15106,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@8.3.0: dependencies: '@babel/code-frame': 7.29.0 @@ -14163,6 +15284,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + prosemirror-changeset@2.4.1: dependencies: prosemirror-transform: 1.12.0 @@ -14360,6 +15483,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 18.3.28 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): @@ -14466,6 +15607,40 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + rename-keys@1.2.0: {} require-directory@2.1.1: {} @@ -14740,6 +15915,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + spawn-command@0.0.2: {} spawndamnit@3.0.1: @@ -14844,6 +16021,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -14877,6 +16059,14 @@ snapshots: style-mod@4.1.3: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.7): dependencies: client-only: 0.0.1 @@ -15219,6 +16409,10 @@ snapshots: node-addon-api: 8.7.0 node-gyp-build: 4.8.4 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -15337,6 +16531,39 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} universalify@2.0.1: {} @@ -15414,6 +16641,16 @@ snapshots: version-range@4.15.0: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 @@ -15524,7 +16761,7 @@ snapshots: optionalDependencies: vite: 7.3.5(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) - vitest@3.2.4(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@1.21.7)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -15550,6 +16787,7 @@ snapshots: vite-node: 3.2.4(@types/node@20.19.37)(jiti@1.21.7)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 20.19.37 jsdom: 25.0.1 transitivePeerDependencies: @@ -15566,7 +16804,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -15592,6 +16830,7 @@ snapshots: vite-node: 3.2.4(@types/node@20.19.37)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 20.19.37 jsdom: 25.0.1 transitivePeerDependencies: @@ -15812,3 +17051,5 @@ snapshots: '@types/react': 18.3.28 react: 18.3.1 use-sync-external-store: 1.6.0(react@18.3.1) + + zwitch@2.0.4: {} From da27284727b1fa2dba2c598eb776581b2a5bd92c Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 08:24:27 -0700 Subject: [PATCH 19/25] feat(web): present the local desktop app --- apps/web/app/_components/product-dive.tsx | 96 +++++- apps/web/app/globals.css | 398 ++++++++++++++++++++++ apps/web/tsconfig.json | 1 - 3 files changed, 493 insertions(+), 2 deletions(-) diff --git a/apps/web/app/_components/product-dive.tsx b/apps/web/app/_components/product-dive.tsx index 368ba479d..5756c6b94 100644 --- a/apps/web/app/_components/product-dive.tsx +++ b/apps/web/app/_components/product-dive.tsx @@ -1,7 +1,7 @@ import type { Media } from '@/components/media-image'; import { Link } from '@/components/link'; import { MediaImage } from '@/components/media-image'; -import { pluginsHref } from '@/content/links'; +import { desktopRelease, pluginsHref } from '@/content/links'; import { cn } from '@/lib/utils'; interface DiveChapter { @@ -124,6 +124,100 @@ export function ProductDive(): React.ReactElement { ))} + +
+
+ macOS desktop · local by design +

Keep the map beside the code.

+

+ Open a CodeGraphy Workspace, browse Files and Folders, edit a File, and inspect its + Relationships without leaving one focused window. Core owns the Relationship Graph; + your source, settings, and Graph Cache stay on your Mac. +

+
+ Local Core process + Workspace-owned Graph Cache + No source upload +
+
+ {desktopRelease.label} +

+ The app is not a public download yet. Apple signing and notarization must pass + release verification first. +

+
+ + Inspect the desktop source + +
+ +
+ + + +
+
); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 24a1aba83..c5f95c8fd 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -596,6 +596,361 @@ button { transition: opacity 180ms ease-out; } +.desktop-dive { + display: grid; + grid-template-columns: minmax(17rem, 0.42fr) minmax(0, 1fr); + gap: clamp(2.5rem, 6vw, 6.5rem); + align-items: center; + margin-top: clamp(4rem, 8vw, 8rem); + padding: clamp(1.25rem, 2vw, 2rem); + border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--border)); + border-radius: 2rem; + background: + radial-gradient(circle at 82% 18%, color-mix(in srgb, var(--primary) 13%, transparent), transparent 34rem), + color-mix(in srgb, var(--depth-surface) 82%, var(--background)); + box-shadow: var(--elevation-xs); + scroll-margin-top: 6rem; +} + +.desktop-dive-copy { + padding: clamp(0.5rem, 2vw, 1.75rem); +} + +.desktop-dive-kicker, +.desktop-dive-facts, +.desktop-dive-status, +.desktop-pane-label, +.desktop-workbench-titlebar, +.desktop-workbench-status, +.desktop-graph-header { + font-family: var(--font-family-mono); +} + +.desktop-dive-kicker { + color: var(--primary); + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.desktop-dive-copy h3 { + max-width: 30rem; + margin-top: 1rem; + font-size: clamp(2.8rem, 4.8vw, 5.4rem); + font-weight: 500; + line-height: 0.94; + letter-spacing: -0.045em; +} + +.desktop-dive-copy > p { + max-width: 35rem; + margin-top: 1.5rem; + color: var(--muted-foreground); + font-size: 1rem; + line-height: 1.75; +} + +.desktop-dive-facts { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1.5rem; + color: var(--muted-foreground); + font-size: 0.6rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.desktop-dive-facts span { + padding: 0.45rem 0.6rem; + border: 1px solid var(--border); + border-radius: 999px; + background: color-mix(in srgb, var(--card) 76%, transparent); +} + +.desktop-dive-release { + margin-top: 1.5rem; + padding: 1rem; + border-left: 2px solid #d49a4a; + background: color-mix(in srgb, #d49a4a 7%, transparent); +} + +.desktop-dive-status { + color: color-mix(in srgb, #d49a4a 82%, var(--foreground)); + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.desktop-dive-release p { + max-width: 34rem; + margin-top: 0.45rem; + color: var(--muted-foreground); + font-size: 0.83rem; + line-height: 1.55; +} + +.desktop-dive-action { + margin-top: 0.8rem; +} + +.desktop-workbench { + min-width: 0; + overflow: hidden; + border: 1px solid rgb(255 255 255 / 12%); + border-radius: 1.15rem; + background: #21252b; + color: #abb2bf; + box-shadow: 0 2.2rem 5rem rgb(3 10 18 / 28%), 0 0 0 1px rgb(0 0 0 / 35%); +} + +.desktop-workbench-titlebar { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 0.75rem; + align-items: center; + min-height: 2.65rem; + padding-inline: 0.85rem; + border-bottom: 1px solid #181a1f; + background: #282c34; + color: #9da5b4; + font-size: 0.57rem; + letter-spacing: 0.03em; +} + +.desktop-workbench-titlebar > span:last-child { + justify-self: end; + color: #6f7784; +} + +.desktop-traffic-lights { + display: flex; + gap: 0.42rem; +} + +.desktop-traffic-lights i { + width: 0.62rem; + height: 0.62rem; + border-radius: 50%; + background: #ff5f57; +} + +.desktop-traffic-lights i:nth-child(2) { + background: #febc2e; +} + +.desktop-traffic-lights i:nth-child(3) { + background: #28c840; +} + +.desktop-workbench-body { + display: grid; + grid-template-columns: minmax(8.5rem, 0.78fr) minmax(12rem, 1.2fr) minmax(13rem, 1.4fr); + min-height: 24rem; +} + +.desktop-files, +.desktop-editor { + border-right: 1px solid #181a1f; +} + +.desktop-files { + padding: 0.8rem 0.55rem; + background: #282c34; +} + +.desktop-pane-label { + display: block; + padding: 0.2rem 0.55rem 0.8rem; + color: #7f8795; + font-size: 0.55rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.desktop-tree-row { + display: block; + overflow: hidden; + padding: 0.33rem 0.5rem; + border-radius: 0.28rem; + font-size: 0.68rem; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.desktop-tree-folder { + color: #c0c7d2; +} + +.desktop-tree-file::before { + content: ''; + display: inline-block; + width: 0.42rem; + height: 0.5rem; + margin-right: 0.42rem; + border: 1px solid #56b6c2; + border-radius: 0.08rem; + vertical-align: -0.04rem; +} + +.desktop-tree-indent { + padding-left: 1rem; +} + +.desktop-tree-indent-2 { + padding-left: 1.75rem; +} + +.desktop-tree-active { + background: #3e4451; + color: #f1f3f5; +} + +.desktop-editor { + min-width: 0; + background: #21252b; +} + +.desktop-tab { + display: flex; + justify-content: space-between; + align-items: center; + min-height: 2.2rem; + padding-inline: 0.8rem; + border-top: 2px solid #56b6c2; + border-bottom: 1px solid #181a1f; + background: #21252b; + color: #d7dae0; + font-size: 0.65rem; +} + +.desktop-code { + display: grid; + gap: 0.34rem; + padding: 1rem 0.7rem; + font-family: var(--font-family-mono); + font-size: clamp(0.52rem, 0.7vw, 0.7rem); + line-height: 1.35; + white-space: nowrap; +} + +.desktop-code span { + overflow: hidden; + text-overflow: clip; +} + +.desktop-code b { + display: inline-block; + width: 1.7rem; + margin-right: 0.45rem; + color: #5c6370; + font-weight: 400; + text-align: right; +} + +.desktop-code i { + color: #c678dd; + font-style: normal; +} + +.desktop-code em { + color: #98c379; + font-style: normal; +} + +.desktop-code strong { + color: #61afef; + font-weight: 400; +} + +.desktop-graph { + position: relative; + min-width: 0; + overflow: hidden; + background: + radial-gradient(circle at center, rgb(86 182 194 / 8%), transparent 58%), + #1d2026; +} + +.desktop-graph-header { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem; + color: #7f8795; + font-size: 0.53rem; +} + +.desktop-graph-header span:first-child { + color: #abb2bf; +} + +.desktop-graph-map { + position: absolute; + inset: 2.25rem 0.25rem 0; + width: calc(100% - 0.5rem); + height: calc(100% - 2.25rem); +} + +.desktop-relationships path { + fill: none; + stroke: #4b5362; + stroke-width: 1.4; +} + +.desktop-nodes rect { + fill: #2c323c; + stroke: #56b6c2; + stroke-width: 2; +} + +.desktop-nodes .desktop-node-selected { + fill: #3b4651; + stroke: #88d4dc; + stroke-width: 3; + filter: drop-shadow(0 0 10px rgb(86 182 194 / 38%)); +} + +.desktop-graph-selection { + position: absolute; + right: 1rem; + bottom: 0.8rem; + padding: 0.35rem 0.55rem; + border: 1px solid rgb(86 182 194 / 26%); + border-radius: 0.35rem; + background: rgb(33 37 43 / 82%); + color: #b6dce0; + font-family: var(--font-family-mono); + font-size: 0.54rem; +} + +.desktop-workbench-status { + display: flex; + justify-content: space-between; + gap: 0.75rem; + min-height: 1.75rem; + padding: 0.45rem 0.75rem; + border-top: 1px solid #181a1f; + background: #282c34; + color: #7f8795; + font-size: 0.52rem; +} + +.desktop-workbench-status span:first-child::before { + content: ''; + display: inline-block; + width: 0.42rem; + height: 0.42rem; + margin-right: 0.4rem; + border-radius: 50%; + background: #56b6c2; +} + .dark .depth-media img { outline-color: rgb(255 255 255 / 10%); } @@ -707,6 +1062,10 @@ button { width: 100%; justify-self: stretch; } + + .desktop-dive { + grid-template-columns: minmax(0, 1fr); + } } @media (max-width: 640px) { @@ -757,6 +1116,45 @@ button { border-radius: 1.1rem; } + .desktop-dive { + margin-inline: -0.5rem; + padding: 0.75rem; + border-radius: 1.25rem; + } + + .desktop-dive-copy { + padding: 1rem; + } + + .desktop-workbench-body { + grid-template-columns: 6.5rem minmax(0, 1fr); + min-height: 0; + } + + .desktop-files, + .desktop-editor { + min-height: 20rem; + } + + .desktop-editor { + border-right: 0; + } + + .desktop-graph { + grid-column: 1 / -1; + min-height: 13rem; + border-top: 1px solid #181a1f; + } + + .desktop-workbench-titlebar > span:last-child, + .desktop-workbench-status span:last-child { + display: none; + } + + .desktop-workbench-titlebar { + grid-template-columns: 1fr auto; + } + } @media (prefers-reduced-motion: reduce) { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index d6d675730..9cfbbc7c1 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -12,7 +12,6 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", - "preserveSymlinks": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", From db9dad8d80a0ded9f9502970e4fe50ae492e013a Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 08:48:29 -0700 Subject: [PATCH 20/25] fix(ci): preserve package type boundaries --- apps/desktop/src/core-sidecar.test.ts | 2 +- apps/web/tsconfig.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/core-sidecar.test.ts b/apps/desktop/src/core-sidecar.test.ts index 5aab0f277..d157999e0 100644 --- a/apps/desktop/src/core-sidecar.test.ts +++ b/apps/desktop/src/core-sidecar.test.ts @@ -53,7 +53,7 @@ describe('Core desktop sidecar', () => { }); expect(result.status).toBe(0); - expect(result.stdout.trim().split('\n').map(line => JSON.parse(line))).toEqual([ + expect(result.stdout.trim().split('\n').map(line => JSON.parse(line) as unknown)).toEqual([ expect.objectContaining({ kind: 'response', id: 1, outcome: 'success' }), ]); expect(result.stderr).toContain('[plugin] initialized'); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 9cfbbc7c1..d6d675730 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -12,6 +12,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", + "preserveSymlinks": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", From 6371c45862fc8a5627bacfbc274a42eb87f28740 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 09:32:05 -0700 Subject: [PATCH 21/25] fix(desktop): coalesce rapid file navigation --- apps/desktop/src/components/FileTree.test.tsx | 54 +++++++++++-- apps/desktop/src/components/FileTree.tsx | 17 ++-- apps/desktop/src/useDesktopWorkspace.test.tsx | 49 ++++++++++++ apps/desktop/src/useDesktopWorkspace.ts | 78 +++++++++++++++---- 4 files changed, 169 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/components/FileTree.test.tsx b/apps/desktop/src/components/FileTree.test.tsx index 29862eb26..6cfe8f9b2 100644 --- a/apps/desktop/src/components/FileTree.test.tsx +++ b/apps/desktop/src/components/FileTree.test.tsx @@ -38,15 +38,25 @@ function setInputValue(input: HTMLInputElement, value: string): void { } describe('File hierarchy keyboard and filter behavior', () => { - const animationFrames: FrameRequestCallback[] = []; + const animationFrames = new Map(); + let animationFrameId = 0; + + const flushAnimationFrames = (): void => { + const callbacks = [...animationFrames.values()]; + animationFrames.clear(); + callbacks.forEach(callback => callback(0)); + }; beforeEach(() => { - animationFrames.length = 0; + animationFrames.clear(); + animationFrameId = 0; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { - animationFrames.push(callback); - return animationFrames.length; + const id = ++animationFrameId; + animationFrames.set(id, callback); + return id; }); + vi.stubGlobal('cancelAnimationFrame', (id: number) => animationFrames.delete(id)); Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { configurable: true, value: vi.fn(), @@ -76,11 +86,11 @@ describe('File hierarchy keyboard and filter behavior', () => { ); await act(async () => { if (main) key(main, 'ArrowDown'); }); - await act(async () => animationFrames.splice(0).forEach(callback => callback(0))); + await act(async () => flushAnimationFrames()); const graphFolder = host.querySelector('[data-tree-path="src/graph"]'); expect(document.activeElement).toBe(graphFolder); await act(async () => { if (graphFolder) key(graphFolder, 'ArrowRight'); }); - await act(async () => animationFrames.splice(0).forEach(callback => callback(0))); + await act(async () => flushAnimationFrames()); expect(document.activeElement).toBe( host.querySelector('[data-tree-path="src/graph/camera.ts"]'), ); @@ -89,6 +99,36 @@ describe('File hierarchy keyboard and filter behavior', () => { await act(async () => root.unmount()); }); + it('coalesces rapid keyboard navigation without replaying focus after key release', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + const onSelect = vi.fn(); + await act(async () => root.render( + , + )); + const src = host.querySelector('[data-tree-path="src"]'); + await act(async () => src?.focus()); + + await act(async () => { + for (let index = 0; index < 3; index += 1) { + const activeItem = document.activeElement; + if (activeItem) key(activeItem, 'ArrowDown', { repeat: index > 0 }); + } + }); + + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/graph/camera.ts"]'), + ); + expect(onSelect).toHaveBeenLastCalledWith('src/graph/camera.ts'); + expect(animationFrames).toHaveLength(1); + await act(async () => flushAnimationFrames()); + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/graph/camera.ts"]'), + ); + await act(async () => root.unmount()); + }); + it('filters relative paths, keeps ancestors, reports empty results, and Escape restores the tree', async () => { const host = document.createElement('div'); document.body.append(host); @@ -115,7 +155,7 @@ describe('File hierarchy keyboard and filter behavior', () => { }); expect(host.textContent).toContain('No Files or Folders match'); await act(async () => { if (filter) keyUp(filter, 'Escape'); }); - await act(async () => animationFrames.splice(0).forEach(callback => callback(0))); + await act(async () => flushAnimationFrames()); expect(host.querySelectorAll('[role="treeitem"]')).toHaveLength(4); expect(document.activeElement).toBe( host.querySelector('[data-tree-path="src/main.ts"]'), diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx index 9addde08b..f522fe469 100644 --- a/apps/desktop/src/components/FileTree.tsx +++ b/apps/desktop/src/components/FileTree.tsx @@ -125,6 +125,7 @@ export function FileTree({ const browserRef = useRef(null); const filterRef = useRef(null); const treeRef = useRef(null); + const scrollFrameRef = useRef(); const typeAheadRef = useRef(''); const typeAheadTimerRef = useRef(); const expandedPaths = useMemo(() => { @@ -155,16 +156,22 @@ export function FileTree({ : allPaths[0]; useEffect(() => () => { + if (scrollFrameRef.current !== undefined) cancelAnimationFrame(scrollFrameRef.current); if (typeAheadTimerRef.current !== undefined) window.clearTimeout(typeAheadTimerRef.current); }, []); const focusPath = (path: string | undefined): void => { if (!path) return; setFocusedPath(path); - requestAnimationFrame(() => { - const target = treeItems(treeRef.current ?? browserRef.current ?? document.body) - .find(item => item.dataset.treePath === path); - target?.focus(); + const findTarget = (): HTMLButtonElement | undefined => treeItems( + treeRef.current ?? browserRef.current ?? document.body, + ).find(item => item.dataset.treePath === path); + findTarget()?.focus(); + if (scrollFrameRef.current !== undefined) cancelAnimationFrame(scrollFrameRef.current); + scrollFrameRef.current = requestAnimationFrame(() => { + scrollFrameRef.current = undefined; + const target = findTarget(); + if (document.activeElement?.closest('[role="tree"]') !== treeRef.current) target?.focus(); target?.scrollIntoView?.({ block: 'nearest' }); }); }; @@ -186,7 +193,7 @@ export function FileTree({ }; const focusTreeFromFilter = (): void => { - requestAnimationFrame(() => focusPath(activePath)); + focusPath(activePath); }; const clearFilterAndRestoreFocus = (): void => { diff --git a/apps/desktop/src/useDesktopWorkspace.test.tsx b/apps/desktop/src/useDesktopWorkspace.test.tsx index 216b9835a..2ec8d992a 100644 --- a/apps/desktop/src/useDesktopWorkspace.test.tsx +++ b/apps/desktop/src/useDesktopWorkspace.test.tsx @@ -98,4 +98,53 @@ describe('desktop document and graph selection ownership', () => { expect(current().dirty).toBe(false); await act(async () => root.unmount()); }); + + it('coalesces rapid File selections to the latest Rust read', async () => { + const pendingReads = new Map void>(); + bridge.readWorkspaceFile.mockImplementation((path: string) => new Promise((resolve) => { + pendingReads.set(path, resolve); + })); + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render()); + const paths = Array.from({ length: 20 }, (_, index) => `src/file-${index}.ts`); + + await act(async () => { + for (const path of paths) void current().selectFile(path); + await Promise.resolve(); + }); + + expect(bridge.readWorkspaceFile).toHaveBeenCalledTimes(1); + expect(bridge.readWorkspaceFile).toHaveBeenLastCalledWith(paths[0]); + await act(async () => { + pendingReads.get(paths[0])?.({ + path: paths[0], + content: '// superseded', + revision: 'revision-first', + }); + await Promise.resolve(); + }); + expect(bridge.readWorkspaceFile).toHaveBeenCalledTimes(2); + expect(bridge.readWorkspaceFile).toHaveBeenLastCalledWith(paths.at(-1)); + + const lastPath = paths.at(-1); + if (!lastPath) throw new Error('The rapid selection fixture is empty.'); + await act(async () => { + pendingReads.get(lastPath)?.({ + path: lastPath, + content: '// latest', + revision: 'revision-last', + }); + await Promise.resolve(); + }); + expect(current().document?.path).toBe(lastPath); + expect(current().draft).toBe('// latest'); + expect(bridge.readWorkspaceFile).toHaveBeenCalledTimes(2); + await act(async () => root.unmount()); + }); }); diff --git a/apps/desktop/src/useDesktopWorkspace.ts b/apps/desktop/src/useDesktopWorkspace.ts index 0b619643a..31ba6d95d 100644 --- a/apps/desktop/src/useDesktopWorkspace.ts +++ b/apps/desktop/src/useDesktopWorkspace.ts @@ -41,6 +41,12 @@ interface FileSwitchMetrics { samples: number[]; } +interface FileOpenIntent { + path: string; + requestId: number; + startedAt: number; +} + interface DesktopWorkspaceState { document?: FileDocument; draft: string; @@ -186,6 +192,9 @@ export function useDesktopWorkspace() { const [state, dispatch] = useReducer(desktopWorkspaceReducer, initialState); const workspaceRequestRef = useRef(0); const fileRequestRef = useRef(0); + const fileOpenQueueRef = useRef(); + const fileReadPromiseRef = useRef>(); + const requestedFilePathRef = useRef(); const graphSettingsDirtyRef = useRef(false); const graphSettingsRef = useRef(DEFAULT_GRAPH_PHYSICS_SETTINGS); const graphSettingsTimerRef = useRef>(); @@ -232,7 +241,11 @@ export function useDesktopWorkspace() { if (requestId !== workspaceRequestRef.current) return false; const changed = workspaceRootRef.current !== result.workspaceRoot; workspaceRootRef.current = result.workspaceRoot; - if (changed) fileRequestRef.current += 1; + if (changed) { + fileRequestRef.current += 1; + fileOpenQueueRef.current = undefined; + requestedFilePathRef.current = undefined; + } dispatch({ type: 'workspace_loaded', changed, result }); if (changed) { const settings = await readDesktopGraphSettings(); @@ -312,32 +325,59 @@ export function useDesktopWorkspace() { const openFile = useCallback(async (path: string): Promise => { const requestId = ++fileRequestRef.current; - const startedAt = performance.now(); + requestedFilePathRef.current = path; + fileOpenQueueRef.current = { path, requestId, startedAt: performance.now() }; dispatch({ type: 'file_pending', path }); - try { - const document = await readWorkspaceFile(path); - if (requestId !== fileRequestRef.current) return; - dispatch({ type: 'file_opened', document }); - dispatch({ type: 'graph_selected', id: path }); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (requestId !== fileRequestRef.current) return; - dispatch({ type: 'file_metrics', elapsed: performance.now() - startedAt, path }); - })); - } catch (error) { - if (requestId !== fileRequestRef.current) return; - dispatch({ type: 'file_pending' }); - reportError(error); + if (!fileReadPromiseRef.current) { + const readFiles = async (): Promise => { + while (fileOpenQueueRef.current) { + const intent = fileOpenQueueRef.current; + fileOpenQueueRef.current = undefined; + try { + const document = await readWorkspaceFile(intent.path); + if (intent.requestId !== fileRequestRef.current) continue; + dispatch({ type: 'file_opened', document }); + dispatch({ type: 'graph_selected', id: intent.path }); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (intent.requestId !== fileRequestRef.current) return; + dispatch({ + type: 'file_metrics', + elapsed: performance.now() - intent.startedAt, + path: intent.path, + }); + })); + } catch (error) { + if (intent.requestId !== fileRequestRef.current) continue; + requestedFilePathRef.current = undefined; + dispatch({ type: 'file_pending' }); + reportError(error); + } + } + }; + const readPromise = readFiles(); + fileReadPromiseRef.current = readPromise; + void readPromise.finally(() => { + if (fileReadPromiseRef.current === readPromise) fileReadPromiseRef.current = undefined; + }); } + await fileReadPromiseRef.current; }, [reportError]); const selectFile = useCallback(async (path: string): Promise => { - if (state.document?.path === path || state.pendingFilePath === path) return; + if (requestedFilePathRef.current === path) return; if (dirty) { dispatch({ type: 'file_action', action: { kind: 'open', path } }); return; } + if (state.document?.path === path) { + fileRequestRef.current += 1; + fileOpenQueueRef.current = undefined; + requestedFilePathRef.current = path; + dispatch({ type: 'file_pending' }); + return; + } await openFile(path); - }, [dirty, openFile, state.document?.path, state.pendingFilePath]); + }, [dirty, openFile, state.document?.path]); const selectGraphNode = useCallback((id: string | undefined): void => { dispatch({ type: 'graph_selected', id }); @@ -348,6 +388,8 @@ export function useDesktopWorkspace() { const discardCurrentDocument = useCallback((): void => { fileRequestRef.current += 1; + fileOpenQueueRef.current = undefined; + requestedFilePathRef.current = undefined; dispatch({ type: 'file_closed' }); }, []); @@ -385,6 +427,8 @@ export function useDesktopWorkspace() { await flushGraphSettings(); workspaceRequestRef.current += 1; fileRequestRef.current += 1; + fileOpenQueueRef.current = undefined; + requestedFilePathRef.current = undefined; try { await closeWorkspace(); workspaceRootRef.current = undefined; From ec4a3c91af2a3e06a0686bd9de28f671478beca5 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 09:44:22 -0700 Subject: [PATCH 22/25] refactor(graph-renderer): own shared visual semantics --- .changeset/calm-renderers-unite.md | 5 ++++ CONTEXT.md | 2 +- README.md | 5 ++-- apps/desktop/README.md | 2 +- apps/desktop/package.json | 1 - apps/desktop/src/bridge.ts | 2 +- apps/desktop/src/components/GraphPanel.tsx | 2 +- .../src/components/GraphSettingsPopover.tsx | 2 +- apps/desktop/src/desktopGraphVisuals.test.ts | 2 +- apps/desktop/src/desktopGraphVisuals.ts | 2 +- apps/desktop/src/materialIconTheme.ts | 4 +-- apps/desktop/src/useDesktopGraphRenderer.ts | 2 +- apps/desktop/src/useDesktopWorkspace.ts | 2 +- packages/extension/package.json | 1 - .../src/extension/actions/resetSettings.ts | 2 +- .../groups/defaults/materialTheme/files.ts | 2 +- .../groups/defaults/materialTheme/folders.ts | 2 +- .../groups/defaults/materialTheme/groups.ts | 2 +- .../groups/defaults/materialTheme/icons.ts | 2 +- .../groups/defaults/materialTheme/manifest.ts | 2 +- .../groups/defaults/materialTheme/model.ts | 2 +- .../groups/defaults/materialTheme/schema.ts | 2 +- .../groups/defaults/materialTheme/view.ts | 2 +- .../groups/defaults/symbols/icons.ts | 2 +- .../graphView/provider/physicsSettings.ts | 2 +- .../graphView/provider/settingsState.ts | 2 +- .../graphView/settings/physics/reader.ts | 2 +- .../graphView/settings/physics/updates.ts | 2 +- .../extension/graphView/settings/reader.ts | 2 +- .../extension/graphView/settings/snapshot.ts | 2 +- .../graphView/webview/dispatch/primary.ts | 2 +- .../graphView/webview/messages/physics.ts | 2 +- .../webview/providerMessages/listener.ts | 2 +- .../webview/settingsMessages/direction.ts | 2 +- .../src/extension/repoSettings/defaults.ts | 4 +-- .../repoSettings/store/model/migrate.ts | 2 +- .../defaults/nodeTypes/structural.ts | 2 +- .../extension/src/shared/mock/transforms.ts | 2 +- .../src/shared/protocol/extensionToWebview.ts | 2 +- .../src/shared/protocol/webviewToExtension.ts | 2 +- .../extension/src/shared/settings/snapshot.ts | 2 +- .../webview/app/shell/visibleGraphResponse.ts | 2 +- .../components/graph/model/node/appearance.ts | 2 +- .../components/graph/model/node/display.ts | 2 +- .../components/graph/model/node/sizing.ts | 2 +- .../components/graph/model/node/styleBuild.ts | 2 +- .../components/graph/model/sizing/fileSize.ts | 2 +- .../graph/rendering/link/colors/model.ts | 2 +- .../graph/rendering/link/metrics.ts | 2 +- .../components/graph/rendering/node/label.ts | 2 +- .../graph/rendering/node/labelSpriteRaster.ts | 2 +- .../components/graph/rendering/node/media.ts | 2 +- .../graph/rendering/nodes/canvasStyle.ts | 2 +- .../owned2d/layout/collision/radius.ts | 2 +- .../surface/owned2d/layout/runtime/data.ts | 2 +- .../surface/owned2d/layout/runtime/host.ts | 2 +- .../surface/owned2d/layout/runtime/model.ts | 2 +- .../surface/owned2d/layout/runtime/update.ts | 2 +- .../surface/owned2d/plugin/forces/adapters.ts | 2 +- .../surface/owned2d/plugin/forces/context.ts | 2 +- .../surface/owned2d/plugin/forces/model.ts | 2 +- .../surface/owned2d/view/surface/contracts.ts | 2 +- .../components/graph/runtime/tooltip/rect.ts | 2 +- .../runtime/use/indicators/nodeAppearance.ts | 2 +- .../settingsPanel/forces/Section.tsx | 2 +- .../settingsPanel/forces/persistence.ts | 2 +- .../src/webview/export/json/build/nodes.ts | 2 +- .../src/webview/export/shared/context.ts | 2 +- .../webview/export/svg/link/element/build.ts | 2 +- .../webview/export/svg/node/image/overlay.ts | 2 +- .../webview/graphControls/filtering/nodes.ts | 2 +- .../filtering/rules/nodeLegend/apply.ts | 2 +- .../src/webview/store/initialState.ts | 2 +- .../src/webview/store/messageTypes.ts | 2 +- packages/extension/src/webview/store/state.ts | 2 +- .../materialTheme/extensionMatch.test.ts | 2 +- .../materialTheme/fileExtension.test.ts | 2 +- .../defaults/materialTheme/fileName.test.ts | 2 +- .../defaults/materialTheme/files.test.ts | 2 +- .../defaults/materialTheme/folderName.test.ts | 2 +- .../defaults/materialTheme/folders.test.ts | 2 +- .../materialTheme/languageFallback.test.ts | 2 +- .../defaults/materialTheme/pathMatch.test.ts | 2 +- .../groups/defaults/materialTheme/svg.test.ts | 2 +- .../provider/physicsSettings.test.ts | 2 +- .../graphView/provider/settingsState.test.ts | 2 +- .../settingsStateDefaultDependencies.test.ts | 2 +- .../graphView/settings/physics/reader.test.ts | 2 +- .../graphView/settings/reader.test.ts | 2 +- .../settings/snapshotMessages.test.ts | 2 +- .../extension/repoSettings/defaults.test.ts | 2 +- ...ts => graphRenderer.package-resolution.ts} | 4 +-- .../extension/tests/shared/fileColors.test.ts | 2 +- .../graphControls/defaults/nodeTypes.test.ts | 2 +- .../extension/tests/shared/mockData.test.ts | 2 +- ...andtorendersthegraphwitheffective.test.tsx | 2 +- ...atewhentodoesnotrendertheembedded.test.tsx | 2 +- .../webview/app/shell/behavior/fixture.tsx | 2 +- .../webview/app/shell/mutations/fixture.tsx | 2 +- .../app/shell/runtime.mutations.test.tsx | 2 +- .../tests/webview/app/shell/view/fixture.ts | 2 +- .../webview/export/shared/context.test.ts | 2 +- .../webview/export/svg/link/document.test.ts | 2 +- .../export/svg/link/element/build.test.ts | 2 +- .../webview/export/svg/link/links.test.ts | 2 +- .../tests/webview/graph/drag.test.tsx | 4 +-- .../graph/marqueeSelection/view.test.tsx | 2 +- .../tests/webview/graph/model/index.test.ts | 2 +- .../model/node/display.mutations.test.ts | 2 +- .../model/node/display.mutations2.test.ts | 2 +- .../webview/graph/model/node/display.test.ts | 2 +- .../sizing/calculations.mutations.test.ts | 2 +- .../graph/model/sizing/calculations.test.ts | 2 +- ...rmalizesinvaliddirectioncolorsback.test.ts | 2 +- .../webview/graph/rendering/links.test.ts | 2 +- .../owned2d/layout/runtime/model.test.ts | 2 +- .../surface/owned2d/view/surface/fixture.ts | 2 +- .../runtime/physics/root/settings.test.ts | 2 +- .../graph/runtime/tooltip/rect.test.ts | 2 +- .../indicators/nodeAppearance.apply.test.ts | 2 +- .../use/indicators/nodeAppearance.fixture.ts | 2 +- .../webview/graph/viewport/shellFixture.tsx | 2 +- .../graphControls/filtering/nodes.test.ts | 6 ++--- .../tests/webview/search/filtering.test.ts | 2 +- .../webview/search/filtering/rules.test.ts | 2 +- .../search/filtering/rules/nodes.test.ts | 2 +- .../webview/settingsPanel/Drawer.test.tsx | 4 +-- .../settingsPanel/forces/Section.test.tsx | 2 +- .../settingsPanel/forces/persistence.test.ts | 2 +- .../webview/store/state/defaults.test.ts | 2 +- .../store/state/runtime/graphMessages.test.ts | 2 +- packages/graph-renderer/README.md | 7 ++++-- packages/graph-renderer/build.mjs | 7 ++++-- packages/graph-renderer/package.json | 10 +++++++- .../src/visuals}/graph.ts | 0 .../src/visuals}/index.ts | 0 .../src/visuals}/materialTheme.ts | 0 .../src/visuals}/physics.ts | 0 .../tests/visuals}/index.test.ts | 4 ++- .../tests/visuals}/physics.test.ts | 4 ++- packages/graph-visuals/package.json | 25 ------------------- packages/graph-visuals/tsconfig.json | 15 ----------- packages/graph-visuals/vitest.config.ts | 9 ------- pnpm-lock.yaml | 15 ----------- 144 files changed, 166 insertions(+), 210 deletions(-) create mode 100644 .changeset/calm-renderers-unite.md rename packages/extension/tests/playwright/{graphVisuals.package-resolution.ts => graphRenderer.package-resolution.ts} (85%) rename packages/{graph-visuals/src => graph-renderer/src/visuals}/graph.ts (100%) rename packages/{graph-visuals/src => graph-renderer/src/visuals}/index.ts (100%) rename packages/{graph-visuals/src => graph-renderer/src/visuals}/materialTheme.ts (100%) rename packages/{graph-visuals/src => graph-renderer/src/visuals}/physics.ts (100%) rename packages/{graph-visuals/tests => graph-renderer/tests/visuals}/index.test.ts (98%) rename packages/{graph-visuals/tests => graph-renderer/tests/visuals}/physics.test.ts (97%) delete mode 100644 packages/graph-visuals/package.json delete mode 100644 packages/graph-visuals/tsconfig.json delete mode 100644 packages/graph-visuals/vitest.config.ts diff --git a/.changeset/calm-renderers-unite.md b/.changeset/calm-renderers-unite.md new file mode 100644 index 000000000..ee914928b --- /dev/null +++ b/.changeset/calm-renderers-unite.md @@ -0,0 +1,5 @@ +--- +"@codegraphy-dev/graph-renderer": minor +--- + +Own shared Material icon matching, Node geometry, connection sizing, and force-control semantics in the renderer package. diff --git a/CONTEXT.md b/CONTEXT.md index 4777e16ce..e27b68b44 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -191,7 +191,7 @@ Extension chrome inherits the active VS Code theme. Graph Data Color may encode - `packages/extension` owns the VS Code product surface. - `packages/tldraw` owns the tldraw offline product surface and launcher. - `packages/graph-renderer` owns graph drawing and physics. -- `packages/graph-visuals` owns deterministic visual semantics shared by interfaces, including Material Icon Theme matching, graph sizing metrics, icon geometry, and renderer input defaults. Each interface still owns and resolves its host theme colors. +- `packages/graph-renderer` owns WebGPU drawing, WebAssembly physics, and deterministic visual semantics shared by interfaces, including Material Icon Theme matching, graph sizing metrics, icon geometry, and renderer input defaults. Each interface still owns and resolves its host theme colors. - `packages/plugin-api` owns public Core plugin contracts. - `packages/extension-plugin-api` owns public VS Code Extension plugin contracts. - `packages/plugin-*` own optional Core or interface plugins. diff --git a/README.md b/README.md index 15f105b7b..67ddee976 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ A public `codegraphy/skills` repository will host the skill once published. The macOS desktop app runs Core in a bundled local process and keeps File access and child-process control in Rust. Its webview combines a File hierarchy, CodeMirror, and the existing renderer. The VS Code extension connects Core to the editor lifecycle and React Graph View. The tldraw interface connects Core data and shared physics to native tldraw shapes. -`@codegraphy-dev/graph-renderer` owns product-neutral WebGPU drawing and WebAssembly physics. The private `@codegraphy-dev/graph-visuals` package owns CodeGraphy-specific Node appearance, Material icon matching, connection sizing, and force-control semantics shared by the desktop app and VS Code extension. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. +`@codegraphy-dev/graph-renderer` owns WebGPU drawing, WebAssembly physics, Node geometry, Material icon matching, connection sizing, and force-control semantics shared by the desktop app and VS Code extension. Interfaces still own host theme color resolution, settings persistence, and interaction policy. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. | Package | Role | |---|---| @@ -216,8 +216,7 @@ The macOS desktop app runs Core in a bundled local process and keeps File access | [`@codegraphy-dev/core`](./packages/core/README.md) | Shared indexing, cache, plugin, query, and CLI engine. | | [`@codegraphy-dev/extension`](./packages/extension/docs/README.md) | VS Code host and Graph View product integration. | | [`@codegraphy-dev/tldraw`](./packages/tldraw/README.md) | macOS launcher and native tldraw offline canvas integration. | -| [`@codegraphy-dev/graph-renderer`](./packages/graph-renderer/README.md) | WebGPU graph renderer and WebAssembly physics. | -| `@codegraphy-dev/graph-visuals` | Private CodeGraphy visual and force-control semantics shared by interfaces. | +| [`@codegraphy-dev/graph-renderer`](./packages/graph-renderer/README.md) | Shared graph rendering, visual geometry, Material icon matching, and WebAssembly physics. | | [`@codegraphy-dev/plugin-api`](./packages/plugin-api/README.md) | Public TypeScript contracts for Core plugins. | | [`@codegraphy-dev/extension-plugin-api`](./packages/extension-plugin-api/README.md) | Public TypeScript contracts for VS Code Extension plugins. | | `@codegraphy-dev/plugin-*` | Optional plugins for Core or an interface host. | diff --git a/apps/desktop/README.md b/apps/desktop/README.md index c1f089422..28c6bda6f 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -36,7 +36,7 @@ Source Files and the Graph Cache stay in the workspace. The app does not upload The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. -Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-visuals` owns CodeGraphy product policy shared with the extension: Material icon matching, Node appearance, connection sizing, and the named force-control contract. The smaller `@codegraphy-dev/graph-renderer` package remains product-neutral and owns only WebGPU drawing plus WebAssembly physics. Desktop pointer handling stays in the desktop interface. +Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-renderer` owns WebGPU drawing, WebAssembly physics, Material icon matching, Node appearance, connection sizing, and the named force-control contract shared with the extension. Desktop pointer handling, host theme resolution, and settings persistence stay in the desktop interface. This boundary keeps the existing TypeScript Core and WebGPU renderer intact while the small Rust shell provides native macOS integration and safe local File access. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f5c9e349a..9d2e7887c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -22,7 +22,6 @@ "dependencies": { "@codegraphy-dev/core": "workspace:*", "@codegraphy-dev/graph-renderer": "workspace:*", - "@codegraphy-dev/graph-visuals": "workspace:*", "@codemirror/commands": "^6.10.4", "@codemirror/language": "^6.12.4", "@codemirror/language-data": "^6.5.2", diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts index e2d5051d1..8fc8e8715 100644 --- a/apps/desktop/src/bridge.ts +++ b/apps/desktop/src/bridge.ts @@ -4,7 +4,7 @@ import { DEFAULT_GRAPH_PHYSICS_SETTINGS, normalizeGraphPhysicsSettings, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { parseWorkspaceGraphResult, type WorkspaceGraphResult } from './model'; export interface FileDocument { diff --git a/apps/desktop/src/components/GraphPanel.tsx b/apps/desktop/src/components/GraphPanel.tsx index 2dafb98e1..7777f1d0d 100644 --- a/apps/desktop/src/components/GraphPanel.tsx +++ b/apps/desktop/src/components/GraphPanel.tsx @@ -1,4 +1,4 @@ -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { useMemo } from 'react'; import type { DesktopGraph } from '../model'; import { useDesktopGraphRenderer } from '../useDesktopGraphRenderer'; diff --git a/apps/desktop/src/components/GraphSettingsPopover.tsx b/apps/desktop/src/components/GraphSettingsPopover.tsx index 15b8190d7..72b4c1949 100644 --- a/apps/desktop/src/components/GraphSettingsPopover.tsx +++ b/apps/desktop/src/components/GraphSettingsPopover.tsx @@ -1,7 +1,7 @@ import { GRAPH_PHYSICS_CONTROL_LIMITS, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { useEffect, useRef, useState } from 'react'; interface GraphSettingsControl { diff --git a/apps/desktop/src/desktopGraphVisuals.test.ts b/apps/desktop/src/desktopGraphVisuals.test.ts index 044618988..7aa559956 100644 --- a/apps/desktop/src/desktopGraphVisuals.test.ts +++ b/apps/desktop/src/desktopGraphVisuals.test.ts @@ -6,7 +6,7 @@ import { MATERIAL_TRANSPARENT_NODE_COLOR, MUTED_LINK_OPACITY, ORDINARY_LINK_OPACITY, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { describe, expect, it } from 'vitest'; import { createDesktopGraphNodeVisual, diff --git a/apps/desktop/src/desktopGraphVisuals.ts b/apps/desktop/src/desktopGraphVisuals.ts index 0d5f25e5a..741ba6734 100644 --- a/apps/desktop/src/desktopGraphVisuals.ts +++ b/apps/desktop/src/desktopGraphVisuals.ts @@ -14,7 +14,7 @@ import { MUTED_LINK_OPACITY, ORDINARY_LINK_OPACITY, computeConnectionSizes, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { MaterialIconData } from './materialIconTheme'; import type { DesktopGraph, DesktopGraphNode } from './model'; diff --git a/apps/desktop/src/materialIconTheme.ts b/apps/desktop/src/materialIconTheme.ts index 91daadc7f..0e09f4325 100644 --- a/apps/desktop/src/materialIconTheme.ts +++ b/apps/desktop/src/materialIconTheme.ts @@ -6,8 +6,8 @@ import { MATERIAL_TRANSPARENT_NODE_COLOR, toSvgDataUrl, toWhiteSvg, -} from '@codegraphy-dev/graph-visuals'; -import type { MaterialIconManifest } from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; +import type { MaterialIconManifest } from '@codegraphy-dev/graph-renderer/visuals'; const MATERIAL_MANIFEST_URL = './material-icons/dist/material-icons.json'; const MATERIAL_ASSET_ROOT = './material-icons/'; diff --git a/apps/desktop/src/useDesktopGraphRenderer.ts b/apps/desktop/src/useDesktopGraphRenderer.ts index c48edfc85..0995df6e2 100644 --- a/apps/desktop/src/useDesktopGraphRenderer.ts +++ b/apps/desktop/src/useDesktopGraphRenderer.ts @@ -21,7 +21,7 @@ import { graphNodeLabelTop, toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { useCallback, useEffect, useRef, useState } from 'react'; import { DESKTOP_GRAPH_HOVER_SCALE, diff --git a/apps/desktop/src/useDesktopWorkspace.ts b/apps/desktop/src/useDesktopWorkspace.ts index 31ba6d95d..0e5ae5519 100644 --- a/apps/desktop/src/useDesktopWorkspace.ts +++ b/apps/desktop/src/useDesktopWorkspace.ts @@ -2,7 +2,7 @@ import { DEFAULT_GRAPH_PHYSICS_SETTINGS, normalizeGraphPhysicsSetting, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; import { chooseWorkspace, diff --git a/packages/extension/package.json b/packages/extension/package.json index 1de4628f2..cbd489ed6 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -33,7 +33,6 @@ "@codegraphy-dev/core": "workspace:*", "@codegraphy-dev/extension-plugin-api": "workspace:*", "@codegraphy-dev/graph-renderer": "workspace:*", - "@codegraphy-dev/graph-visuals": "workspace:*", "@codegraphy-dev/plugin-api": "workspace:*", "@driftlog/tree-sitter-dart": "1.0.4", "@floating-ui/react": "^0.27.19", diff --git a/packages/extension/src/extension/actions/resetSettings.ts b/packages/extension/src/extension/actions/resetSettings.ts index 82740efa5..a9ccef2be 100644 --- a/packages/extension/src/extension/actions/resetSettings.ts +++ b/packages/extension/src/extension/actions/resetSettings.ts @@ -4,7 +4,7 @@ */ import type { IUndoableAction } from '../undoManager'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { ISettingsSnapshot } from '../../shared/settings/snapshot'; import { getCodeGraphyConfiguration } from '../repoSettings/current'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts index 972dcd9fe..c0c24dd17 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/files.ts @@ -4,7 +4,7 @@ import { findMaterialMatch, getMaterialBaseName, type MaterialMatch, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { isExternalPackageNodeId } from '../../../../pipeline/graph/packageSpecifiers/nodeId'; import type { MaterialThemeCacheEntry } from './model'; import { createMaterialGroup } from './groups'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts index 39ec37038..b6cb43544 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/folders.ts @@ -5,7 +5,7 @@ import { MATERIAL_TRANSPARENT_NODE_COLOR, type MaterialIconManifest, type MaterialMatch, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { collectFolderPaths } from '../../../../../shared/graphControls/nests/folders'; import { isExternalPackageNodeId } from '../../../../pipeline/graph/packageSpecifiers/nodeId'; import type { MaterialThemeCacheEntry } from './model'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts index 4d97a213a..8b8bf86c8 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/groups.ts @@ -2,7 +2,7 @@ import type { IGroup } from '../../../../../shared/settings/groups'; import { MATERIAL_TRANSPARENT_NODE_COLOR, type MaterialMatch, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { MaterialIconData } from './model'; export function createMaterialGroup( diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts index 02d7884fc..0f779f90f 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/icons.ts @@ -4,7 +4,7 @@ import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { MaterialIconData, MaterialThemeCacheEntry } from './model'; export function resolveIconData( diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts index ef1e6ab72..9a59cf91e 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/manifest.ts @@ -5,7 +5,7 @@ import { createMaterialExtensionMatcher, createMaterialPathRuleMatcher, type MaterialIconManifest, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { MaterialThemeCacheEntry } from './model'; import { materialIconManifestSchema } from './schema'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts index be198e655..8b94e8f05 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/model.ts @@ -2,7 +2,7 @@ import type { MaterialExtensionMatcher, MaterialIconManifest, MaterialThemePathMatchers, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; export interface MaterialIconData { color: string; diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts index 6bc1fc105..3e9b2af2b 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import type { MaterialIconManifest } from '@codegraphy-dev/graph-visuals'; +import type { MaterialIconManifest } from '@codegraphy-dev/graph-renderer/visuals'; import { unknownRecordSchema } from '../../../../../shared/values'; const materialStringRecordSchema = unknownRecordSchema.transform((record): Record => diff --git a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts index d7b64a8f2..d29b7fa47 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/materialTheme/view.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { findMaterialMatch } from '@codegraphy-dev/graph-visuals'; +import { findMaterialMatch } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../../shared/graph/contracts'; import type { IGroup } from '../../../../../shared/settings/groups'; import { createMaterialGroup, getManualGroups, getSpecificityScore, sortMaterialGroups } from './groups'; diff --git a/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts b/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts index 3e5afd518..40384dc12 100644 --- a/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts +++ b/packages/extension/src/extension/graphView/groups/defaults/symbols/icons.ts @@ -1,4 +1,4 @@ -import { toSvgDataUrl } from '@codegraphy-dev/graph-visuals'; +import { toSvgDataUrl } from '@codegraphy-dev/graph-renderer/visuals'; export function createMaterialSymbolIconDataUrl(pathData: string): string { return toSvgDataUrl(``); diff --git a/packages/extension/src/extension/graphView/provider/physicsSettings.ts b/packages/extension/src/extension/graphView/provider/physicsSettings.ts index 29ece0914..14f6bcdcf 100644 --- a/packages/extension/src/extension/graphView/provider/physicsSettings.ts +++ b/packages/extension/src/extension/graphView/provider/physicsSettings.ts @@ -1,5 +1,5 @@ import type { ExtensionToWebviewMessage } from '../../../shared/protocol/extensionToWebview'; -import { DEFAULT_GRAPH_PHYSICS_SETTINGS, type GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_GRAPH_PHYSICS_SETTINGS, type GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { readGraphViewPhysicsSettings } from '../settings/physics/reader'; import { resetGraphViewPhysicsSettings, updateGraphViewPhysicsSetting } from '../settings/physics/updates'; import { getCodeGraphyConfiguration } from '../../repoSettings/current'; diff --git a/packages/extension/src/extension/graphView/provider/settingsState.ts b/packages/extension/src/extension/graphView/provider/settingsState.ts index fcc8481dc..e46743251 100644 --- a/packages/extension/src/extension/graphView/provider/settingsState.ts +++ b/packages/extension/src/extension/graphView/provider/settingsState.ts @@ -4,7 +4,7 @@ import type { NodeSizeMode } from '../../../shared/settings/modes'; import type { IViewContext } from '../../../core/views/contracts'; import type { ExtensionToWebviewMessage } from '../../../shared/protocol/extensionToWebview'; import type { IGroup } from '../../../shared/settings/groups'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { getCodeGraphyConfiguration } from '../../repoSettings/current'; import { getGraphViewConfigTarget } from '../settings/reader'; import { applyLoadedGraphViewGroupState } from '../groups/sync'; diff --git a/packages/extension/src/extension/graphView/settings/physics/reader.ts b/packages/extension/src/extension/graphView/settings/physics/reader.ts index 257df5acd..04a636520 100644 --- a/packages/extension/src/extension/graphView/settings/physics/reader.ts +++ b/packages/extension/src/extension/graphView/settings/physics/reader.ts @@ -2,7 +2,7 @@ import type * as vscode from 'vscode'; import { normalizeGraphPhysicsSettings, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; export function readGraphViewPhysicsSettings( config: Pick, diff --git a/packages/extension/src/extension/graphView/settings/physics/updates.ts b/packages/extension/src/extension/graphView/settings/physics/updates.ts index ed083f379..89cb7379d 100644 --- a/packages/extension/src/extension/graphView/settings/physics/updates.ts +++ b/packages/extension/src/extension/graphView/settings/physics/updates.ts @@ -1,7 +1,7 @@ import { normalizeGraphPhysicsSetting, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; interface GraphViewPhysicsConfigurationLike { update(key: string, value: unknown, target?: unknown): PromiseLike; diff --git a/packages/extension/src/extension/graphView/settings/reader.ts b/packages/extension/src/extension/graphView/settings/reader.ts index 6f534aa49..217c49d16 100644 --- a/packages/extension/src/extension/graphView/settings/reader.ts +++ b/packages/extension/src/extension/graphView/settings/reader.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { normalizeHexColor } from '../../../shared/normalizeHexColor'; import type { BidirectionalEdgeMode, DirectionMode } from '../../../shared/settings/modes'; import { DEFAULT_SHOW_MINIMAP } from '../../../shared/settings/defaults'; diff --git a/packages/extension/src/extension/graphView/settings/snapshot.ts b/packages/extension/src/extension/graphView/settings/snapshot.ts index fda5e891b..1810e7efb 100644 --- a/packages/extension/src/extension/graphView/settings/snapshot.ts +++ b/packages/extension/src/extension/graphView/settings/snapshot.ts @@ -1,5 +1,5 @@ import type { NodeSizeMode } from '../../../shared/settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { ISettingsSnapshot } from '../../../shared/settings/snapshot'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../../shared/settings/defaults'; import { readGraphViewSettings } from './reader'; diff --git a/packages/extension/src/extension/graphView/webview/dispatch/primary.ts b/packages/extension/src/extension/graphView/webview/dispatch/primary.ts index 248f922be..e9914c97c 100644 --- a/packages/extension/src/extension/graphView/webview/dispatch/primary.ts +++ b/packages/extension/src/extension/graphView/webview/dispatch/primary.ts @@ -5,7 +5,7 @@ import type { IPluginFilterPatternGroup } from '../../../../shared/protocol/exte import type { IPluginUpdateImpactPolicy } from '@codegraphy-dev/plugin-api'; import type { IGroup } from '../../../../shared/settings/groups'; import type { NodeSizeMode } from '../../../../shared/settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { IViewContext } from '../../../../core/views/contracts'; import type { IFileAnalysisResult } from '../../../../core/plugins/types/contracts'; import type { WorkspaceAnalysisDatabaseSnapshot } from '../../../pipeline/database/cache/storage'; diff --git a/packages/extension/src/extension/graphView/webview/messages/physics.ts b/packages/extension/src/extension/graphView/webview/messages/physics.ts index 07be29d8e..3a88f7e32 100644 --- a/packages/extension/src/extension/graphView/webview/messages/physics.ts +++ b/packages/extension/src/extension/graphView/webview/messages/physics.ts @@ -1,5 +1,5 @@ import type { WebviewToExtensionMessage } from '../../../../shared/protocol/webviewToExtension'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; export interface GraphViewPhysicsMessageHandlers { sendPhysicsSettings(): void; diff --git a/packages/extension/src/extension/graphView/webview/providerMessages/listener.ts b/packages/extension/src/extension/graphView/webview/providerMessages/listener.ts index 99d0dbd73..5f99f6708 100644 --- a/packages/extension/src/extension/graphView/webview/providerMessages/listener.ts +++ b/packages/extension/src/extension/graphView/webview/providerMessages/listener.ts @@ -8,7 +8,7 @@ import type { } from '../../../../shared/protocol/extensionToWebview'; import type { IGroup } from '../../../../shared/settings/groups'; import type { NodeSizeMode } from '../../../../shared/settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { ISettingsSnapshot } from '../../../../shared/settings/snapshot'; import type { IViewContext } from '../../../../core/views/contracts'; import { getUndoManager } from '../../../undoManager'; diff --git a/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts b/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts index 8e88985f9..3a63c3d0c 100644 --- a/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts +++ b/packages/extension/src/extension/graphView/webview/settingsMessages/direction.ts @@ -1,6 +1,6 @@ import type { WebviewToExtensionMessage } from '../../../../shared/protocol/webviewToExtension'; import type { DirectionMode } from '../../../../shared/settings/modes'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { normalizeDirectionColor } from '../../settings/reader'; import type { GraphViewSettingsMessageHandlers, diff --git a/packages/extension/src/extension/repoSettings/defaults.ts b/packages/extension/src/extension/repoSettings/defaults.ts index 75e50f562..9d4620010 100644 --- a/packages/extension/src/extension/repoSettings/defaults.ts +++ b/packages/extension/src/extension/repoSettings/defaults.ts @@ -1,11 +1,11 @@ -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGroup } from '../../shared/settings/groups'; import type { NodeSizeMode } from '../../shared/settings/modes'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; import { DEFAULT_GRAPH_PHYSICS_SETTINGS, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { CODEGRAPHY_MARKDOWN_PLUGIN_ID, type CodeGraphyWorkspaceInterfaceSettings, diff --git a/packages/extension/src/extension/repoSettings/store/model/migrate.ts b/packages/extension/src/extension/repoSettings/store/model/migrate.ts index cd2abae11..f87f0be57 100644 --- a/packages/extension/src/extension/repoSettings/store/model/migrate.ts +++ b/packages/extension/src/extension/repoSettings/store/model/migrate.ts @@ -1,4 +1,4 @@ -import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-renderer/visuals'; import { isPlainObject } from './plainObject'; const CURRENT_SETTINGS_VERSION = 4; diff --git a/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts b/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts index 66eab3086..2900ea95a 100644 --- a/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts +++ b/packages/extension/src/shared/graphControls/defaults/nodeTypes/structural.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; export function createStructuralGraphNodeTypes(): IGraphNodeTypeDefinition[] { return [ diff --git a/packages/extension/src/shared/mock/transforms.ts b/packages/extension/src/shared/mock/transforms.ts index 7ccb6592e..59a71e0b5 100644 --- a/packages/extension/src/shared/mock/transforms.ts +++ b/packages/extension/src/shared/mock/transforms.ts @@ -3,7 +3,7 @@ * @module shared/mockDataTransforms */ -import { getFileColor } from '@codegraphy-dev/graph-visuals'; +import { getFileColor } from '@codegraphy-dev/graph-renderer/visuals'; import type { IFileData } from './fileData'; import type { IGraphEdge, IGraphNode } from '../graph/contracts'; diff --git a/packages/extension/src/shared/protocol/extensionToWebview.ts b/packages/extension/src/shared/protocol/extensionToWebview.ts index 2253513b5..9a618a088 100644 --- a/packages/extension/src/shared/protocol/extensionToWebview.ts +++ b/packages/extension/src/shared/protocol/extensionToWebview.ts @@ -8,7 +8,7 @@ import type { DirectionMode, NodeSizeMode, } from '../settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGroup } from '../settings/groups'; export interface IPluginFilterPatternGroup { diff --git a/packages/extension/src/shared/protocol/webviewToExtension.ts b/packages/extension/src/shared/protocol/webviewToExtension.ts index 15bdaab3a..8eef01337 100644 --- a/packages/extension/src/shared/protocol/webviewToExtension.ts +++ b/packages/extension/src/shared/protocol/webviewToExtension.ts @@ -4,7 +4,7 @@ import type { DirectionMode, NodeSizeMode, } from '../settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; export interface GraphItemCreatePayload { directory: string; diff --git a/packages/extension/src/shared/settings/snapshot.ts b/packages/extension/src/shared/settings/snapshot.ts index eec01ead7..c04031769 100644 --- a/packages/extension/src/shared/settings/snapshot.ts +++ b/packages/extension/src/shared/settings/snapshot.ts @@ -1,6 +1,6 @@ import type { IGroup } from './groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from './modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; export interface ISettingsSnapshot { physics: GraphPhysicsSettings; diff --git a/packages/extension/src/webview/app/shell/visibleGraphResponse.ts b/packages/extension/src/webview/app/shell/visibleGraphResponse.ts index b8432a2f7..7f276a883 100644 --- a/packages/extension/src/webview/app/shell/visibleGraphResponse.ts +++ b/packages/extension/src/webview/app/shell/visibleGraphResponse.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import type { IGraphData } from '../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { postMessage as postWebviewMessage } from '../../vscodeApi'; export function createVisibleGraphStatePayload(graphData: IGraphData | null | undefined) { diff --git a/packages/extension/src/webview/components/graph/model/node/appearance.ts b/packages/extension/src/webview/components/graph/model/node/appearance.ts index 0e679a948..587c2b5a7 100644 --- a/packages/extension/src/webview/components/graph/model/node/appearance.ts +++ b/packages/extension/src/webview/components/graph/model/node/appearance.ts @@ -1,5 +1,5 @@ import type { IGraphNode } from '../../../../../shared/graph/contracts'; -import { GRAPH_NODE_BORDER_WIDTH } from '@codegraphy-dev/graph-visuals'; +import { GRAPH_NODE_BORDER_WIDTH } from '@codegraphy-dev/graph-renderer/visuals'; import type { GraphAppearance } from '../../appearance/model'; import { FALLBACK_MUTED_NODE_COLOR, FAVORITE_BORDER_COLOR } from './display'; diff --git a/packages/extension/src/webview/components/graph/model/node/display.ts b/packages/extension/src/webview/components/graph/model/node/display.ts index cd1bcd43f..b17780553 100644 --- a/packages/extension/src/webview/components/graph/model/node/display.ts +++ b/packages/extension/src/webview/components/graph/model/node/display.ts @@ -1,6 +1,6 @@ import { DEFAULT_DIRECTION_COLOR, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; export const FAVORITE_BORDER_COLOR = '#EAB308'; export const FALLBACK_MUTED_NODE_COLOR = '#6b7280'; diff --git a/packages/extension/src/webview/components/graph/model/node/sizing.ts b/packages/extension/src/webview/components/graph/model/node/sizing.ts index 5f53908ac..b40e2582d 100644 --- a/packages/extension/src/webview/components/graph/model/node/sizing.ts +++ b/packages/extension/src/webview/components/graph/model/node/sizing.ts @@ -1,6 +1,6 @@ import type { IGraphEdge, IGraphNode } from '../../../../../shared/graph/contracts'; import type { NodeSizeMode } from '../../../../../shared/settings/modes'; -import { computeConnectionSizes } from '@codegraphy-dev/graph-visuals'; +import { computeConnectionSizes } from '@codegraphy-dev/graph-renderer/visuals'; import { computeFileSizeSizes } from '../sizing/fileSize'; export function calculateNodeSizes( diff --git a/packages/extension/src/webview/components/graph/model/node/styleBuild.ts b/packages/extension/src/webview/components/graph/model/node/styleBuild.ts index 192faf8c8..ccdf33c52 100644 --- a/packages/extension/src/webview/components/graph/model/node/styleBuild.ts +++ b/packages/extension/src/webview/components/graph/model/node/styleBuild.ts @@ -4,7 +4,7 @@ import { DEFAULT_NODE_SIZE, MAX_NODE_SIZE, MIN_NODE_SIZE, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { adjustColorForLightTheme } from '../../../../theme/useTheme'; import type { GraphAppearance } from '../../appearance/model'; import { graphNodeBorderColor, graphNodeBorderWidth, graphNodeDisplayColor } from './appearance'; diff --git a/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts b/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts index c92a45a9c..64280e548 100644 --- a/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts +++ b/packages/extension/src/webview/components/graph/model/sizing/fileSize.ts @@ -1,5 +1,5 @@ import type { IGraphNode } from '../../../../../shared/graph/contracts'; -import { MAX_NODE_SIZE, MIN_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { MAX_NODE_SIZE, MIN_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; export function computeFileSizeSizes(nodes: IGraphNode[]): Map { const sizes = new Map(); diff --git a/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts b/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts index 93e593e73..cd3cfebb0 100644 --- a/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts +++ b/packages/extension/src/webview/components/graph/rendering/link/colors/model.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { resolveDirectionColor, type FGLink, diff --git a/packages/extension/src/webview/components/graph/rendering/link/metrics.ts b/packages/extension/src/webview/components/graph/rendering/link/metrics.ts index a0bbf9d8a..42d515838 100644 --- a/packages/extension/src/webview/components/graph/rendering/link/metrics.ts +++ b/packages/extension/src/webview/components/graph/rendering/link/metrics.ts @@ -4,7 +4,7 @@ import { LINK_BASE_WIDTH, MUTED_LINK_OPACITY, ORDINARY_LINK_OPACITY, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { resolveLinkEndpointId } from '../../support/linkTargets'; import type { LinkRenderingDependencies } from './contracts'; diff --git a/packages/extension/src/webview/components/graph/rendering/node/label.ts b/packages/extension/src/webview/components/graph/rendering/node/label.ts index 303ea5c15..b50743018 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/label.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/label.ts @@ -1,7 +1,7 @@ import type { NodeDecorationPayload } from '../../../../../shared/plugins/decorations'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../appearance/model'; import { graphDetailOpacity } from '@codegraphy-dev/graph-renderer'; -import { graphNodeLabelTop } from '@codegraphy-dev/graph-visuals'; +import { graphNodeLabelTop } from '@codegraphy-dev/graph-renderer/visuals'; import type { FGNode } from '../../model/build'; import type { NodeLabelSpriteProvider } from './labelSprite'; diff --git a/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts b/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts index b43d2e76c..7180d95c5 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/labelSpriteRaster.ts @@ -2,7 +2,7 @@ import type { NodeLabelSprite } from './labelSprite'; import { GRAPH_NODE_LABEL_FONT, GRAPH_NODE_LABEL_PADDING, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; export function rasterizeNodeLabelSprite(createCanvas: () => HTMLCanvasElement, text: string, color: string, scale: number): { backingBytes: number; sprite: NodeLabelSprite } { const canvas = createCanvas(); diff --git a/packages/extension/src/webview/components/graph/rendering/node/media.ts b/packages/extension/src/webview/components/graph/rendering/node/media.ts index fa4864727..bf9fd1811 100644 --- a/packages/extension/src/webview/components/graph/rendering/node/media.ts +++ b/packages/extension/src/webview/components/graph/rendering/node/media.ts @@ -1,5 +1,5 @@ import type { NodeDecorationPayload } from '../../../../../shared/plugins/decorations'; -import { fileIconSize, folderIconSize } from '@codegraphy-dev/graph-visuals'; +import { fileIconSize, folderIconSize } from '@codegraphy-dev/graph-renderer/visuals'; import { getImage } from '../imageCache'; import { drawShape } from '../shapes/draw/twoDimensional'; import type { WebviewPluginHost } from '../../../../pluginHost/manager'; diff --git a/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts b/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts index 6f05b5951..17f5a4bce 100644 --- a/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts +++ b/packages/extension/src/webview/components/graph/rendering/nodes/canvasStyle.ts @@ -2,7 +2,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, GRAPH_NODE_SELECTION_BORDER_WIDTH, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGNode } from '../../model/build'; import type { NodeCanvasRendererDependencies } from '../node/canvasShared'; import type { OwnedGraphNodeStyle } from '../surface/owned2d/view/surface/contracts'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts index 885cfcfc8..e23a0b221 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius.ts @@ -1,5 +1,5 @@ import type { FGNode } from '../../../../../model/build'; -import { OWNED_GRAPH_COLLISION_RADIUS_PADDING } from '@codegraphy-dev/graph-visuals'; +import { OWNED_GRAPH_COLLISION_RADIUS_PADDING } from '@codegraphy-dev/graph-renderer/visuals'; type CollisionNode = Pick; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts index 90f106f75..8872be109 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/data.ts @@ -1,5 +1,5 @@ import { graphNodeSizeChargeMultiplier, GraphNodeFlag, type GraphLayoutInput } from '@codegraphy-dev/graph-renderer'; -import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../model/build'; import { ownedNodeCollisionRadius } from '../collision/radius'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/host.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/host.ts index a6677f474..7271a11ba 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/host.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/host.ts @@ -1,5 +1,5 @@ import type { MutableRefObject } from 'react'; -import { applyGraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import { applyGraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { OwnedGraphCamera } from '../../camera/runtime/model'; import type { Surface2dProps } from '../../view/surface/contracts'; import type { PointerSession } from '../../interaction/model'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model.ts index 7025456b3..0b2561415 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model.ts @@ -2,7 +2,7 @@ import { createGraphLayoutEngine, type GraphLayoutEngine } from '@codegraphy-dev import { toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../model/build'; import { buildOwnedGraphLayoutData } from './data'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/update.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/update.ts index 6e45703c0..ae9204bab 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/update.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/layout/runtime/update.ts @@ -1,7 +1,7 @@ import { applyGraphPhysicsSettings, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../model/build'; import type { OwnedGraphLayout } from './model'; import { sameOwnedGraphShape } from './comparison'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/adapters.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/adapters.ts index 051773bd4..875d420da 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/adapters.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/adapters.ts @@ -1,7 +1,7 @@ import type { ExtensionGraphViewContributionSet } from '@codegraphy-dev/extension-plugin-api'; import { MAX_GRAPH_COORDINATE, MAX_GRAPH_VELOCITY } from '@codegraphy-dev/graph-renderer'; import type { IGraphViewForceAdapter } from '@codegraphy-dev/extension-plugin-api'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../../../../../shared/graph/contracts'; import type { FGLink, FGNode } from '../../../../../model/build'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/context.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/context.ts index e81fc4375..da2d1b4c7 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/context.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/context.ts @@ -1,4 +1,4 @@ -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { GraphEdgeKind, IGraphData } from '../../../../../../../../shared/graph/contracts'; import type { FGLink, FGNode } from '../../../../../model/build'; diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/model.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/model.ts index 274b81c2e..90051e373 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/model.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/plugin/forces/model.ts @@ -1,5 +1,5 @@ import type { ExtensionGraphViewContributionSet } from '@codegraphy-dev/extension-plugin-api'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../model/build'; import { disposeInstalledForces, diff --git a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts.ts b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts.ts index 45d748a2a..f3406fdb1 100644 --- a/packages/extension/src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts.ts +++ b/packages/extension/src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts.ts @@ -1,6 +1,6 @@ import type { MutableRefObject } from 'react'; import type { ExtensionGraphViewContributionSet } from '@codegraphy-dev/extension-plugin-api'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { DirectionMode, NodeShape2D } from '../../../../../../../../shared/settings/modes'; import type { FGLink, FGNode } from '../../../../../model/build'; import type { GraphSurfaceSharedProps } from '../../../sharedProps'; diff --git a/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts b/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts index 4bad3af59..848ab9368 100644 --- a/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts +++ b/packages/extension/src/webview/components/graph/runtime/tooltip/rect.ts @@ -5,7 +5,7 @@ import type { } from '../../tooltip/model'; import type { FGNode } from '../../model/build'; import { graphNodeScreenRadius } from '@codegraphy-dev/graph-renderer'; -import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; interface GraphTooltipRectOptions { containerRef: MutableRefObject; diff --git a/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts b/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts index f908fc1a7..6dfa1c902 100644 --- a/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts +++ b/packages/extension/src/webview/components/graph/runtime/use/indicators/nodeAppearance.ts @@ -3,7 +3,7 @@ import { type MutableRefObject, } from 'react'; import type { IGraphData } from '../../../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { ThemeKind, adjustColorForLightTheme } from '../../../../../theme/useTheme'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../../appearance/model'; import type { FGLink, FGNode } from '../../../model/build'; diff --git a/packages/extension/src/webview/components/settingsPanel/forces/Section.tsx b/packages/extension/src/webview/components/settingsPanel/forces/Section.tsx index 46e42aa30..356abaa3f 100644 --- a/packages/extension/src/webview/components/settingsPanel/forces/Section.tsx +++ b/packages/extension/src/webview/components/settingsPanel/forces/Section.tsx @@ -2,7 +2,7 @@ import React, { useRef } from 'react'; import { GRAPH_PHYSICS_CONTROL_LIMITS, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { postMessage } from '../../../vscodeApi'; import { useGraphStore } from '../../../store/state'; import { Label } from '../../ui/form/label'; diff --git a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts index 0d5dd2903..a9c077fa5 100644 --- a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts +++ b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts @@ -1,4 +1,4 @@ -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; export type PendingPhysicsMap = Partial>; export type PhysicsTimerMap = Partial< diff --git a/packages/extension/src/webview/export/json/build/nodes.ts b/packages/extension/src/webview/export/json/build/nodes.ts index 6ecf89070..b765e69d0 100644 --- a/packages/extension/src/webview/export/json/build/nodes.ts +++ b/packages/extension/src/webview/export/json/build/nodes.ts @@ -1,5 +1,5 @@ import type { IGraphData } from '../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGroup } from '../../../../shared/settings/groups'; import { globMatch } from '../../../globMatch'; diff --git a/packages/extension/src/webview/export/shared/context.ts b/packages/extension/src/webview/export/shared/context.ts index fc7451e38..b76d28e79 100644 --- a/packages/extension/src/webview/export/shared/context.ts +++ b/packages/extension/src/webview/export/shared/context.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const EXPORT_BACKGROUND_COLOR = '#18181b'; diff --git a/packages/extension/src/webview/export/svg/link/element/build.ts b/packages/extension/src/webview/export/svg/link/element/build.ts index 917423e88..865379355 100644 --- a/packages/extension/src/webview/export/svg/link/element/build.ts +++ b/packages/extension/src/webview/export/svg/link/element/build.ts @@ -1,5 +1,5 @@ import type { SvgExportLink, SvgPosition } from '../../contracts'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const CURVATURE_EPSILON = 0.001; diff --git a/packages/extension/src/webview/export/svg/node/image/overlay.ts b/packages/extension/src/webview/export/svg/node/image/overlay.ts index 876edc300..405e59216 100644 --- a/packages/extension/src/webview/export/svg/node/image/overlay.ts +++ b/packages/extension/src/webview/export/svg/node/image/overlay.ts @@ -2,7 +2,7 @@ import { getImage } from '../../../../components/graph/rendering/imageCache'; import { svgShapePath } from '../../shape/shapes'; import type { SvgExportNode, SvgPosition } from '../../contracts'; import type { NodeShape2D } from '../../../../../shared/settings/modes'; -import { fileIconSize } from '@codegraphy-dev/graph-visuals'; +import { fileIconSize } from '@codegraphy-dev/graph-renderer/visuals'; function buildClipShape(node: SvgExportNode, position: SvgPosition, shape: NodeShape2D): string { if (shape === 'circle') { diff --git a/packages/extension/src/webview/graphControls/filtering/nodes.ts b/packages/extension/src/webview/graphControls/filtering/nodes.ts index 388acd271..253f8e8c5 100644 --- a/packages/extension/src/webview/graphControls/filtering/nodes.ts +++ b/packages/extension/src/webview/graphControls/filtering/nodes.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphNodeTypeDefinition } from '../../../shared/graphControls/contracts'; import { CORE_GRAPH_NODE_TYPES } from '../../../shared/graphControls/defaults/nodeTypes'; import { symbolMatchesScopedDefinition } from '../../../shared/visibleGraph/scope/symbolMatch'; diff --git a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts index 26df7aaa6..482ccdd0f 100644 --- a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts +++ b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts @@ -1,4 +1,4 @@ -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../../shared/graph/contracts'; import { normalizeNodeLegendRules } from './compile'; import type { diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index a60530acd..2806892c5 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_OPTIONS } from './defaults'; import { DEFAULT_DIRECTION_COLOR, DEFAULT_GRAPH_PHYSICS_SETTINGS, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; export const INITIAL_STATE: GraphStateFields = { diff --git a/packages/extension/src/webview/store/messageTypes.ts b/packages/extension/src/webview/store/messageTypes.ts index 506ec5ddf..50b783bc9 100644 --- a/packages/extension/src/webview/store/messageTypes.ts +++ b/packages/extension/src/webview/store/messageTypes.ts @@ -11,7 +11,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, diff --git a/packages/extension/src/webview/store/state.ts b/packages/extension/src/webview/store/state.ts index 896514a34..3031a55d0 100644 --- a/packages/extension/src/webview/store/state.ts +++ b/packages/extension/src/webview/store/state.ts @@ -16,7 +16,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { PendingGroupUpdates, PendingUserGroupsUpdate, diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts index cdaa517a9..d0c873c47 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialExtensionMatcher, findLongestExtensionMatch, findLongestExtensionMatchWithMatcher, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/extensionMatch', () => { it('matches bare extension filenames', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts index 502569404..c22b2348d 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileExtension } from '@codegraphy-dev/graph-visuals'; +import { matchMaterialFileExtension } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/fileExtension', () => { it('matches dotted suffixes and exact extension names', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts index 900759aa2..008012c73 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileName } from '@codegraphy-dev/graph-visuals'; +import { matchMaterialFileName } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/fileName', () => { it('matches basename and nested suffix rules', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts index 7b90054fe..8c158a8ed 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFileGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/files'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { createMaterialPathRuleMatcher } from '@codegraphy-dev/graph-visuals'; +import { createMaterialPathRuleMatcher } from '@codegraphy-dev/graph-renderer/visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts index 8b2493a29..111add3f3 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFolderName } from '@codegraphy-dev/graph-visuals'; +import { matchMaterialFolderName } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/folderName', () => { it('matches basename folder rules across the tree', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts index 758c8f20f..d484ca76b 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFolderGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/folders'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { MATERIAL_TRANSPARENT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { MATERIAL_TRANSPARENT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts index d326de539..8db10d18a 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialLanguageFallback } from '@codegraphy-dev/graph-visuals'; +import { matchMaterialLanguageFallback } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/languageFallback', () => { it('uses language fallback rules when the manifest defines the language id', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts index cf70dc901..02a474e90 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialPathRuleMatcher, findLongestPathMatch, findLongestPathMatchWithMatcher, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/pathMatch', () => { it('matches basename rules case-insensitively', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts index 5c7b53e1d..7fb7d9f54 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts @@ -1,6 +1,6 @@ import { Buffer } from 'node:buffer'; import { describe, expect, it } from 'vitest'; -import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '@codegraphy-dev/graph-visuals'; +import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/svg', () => { it('extracts the most common hex color and falls back when none exist', () => { diff --git a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts index 578ff186c..d666763ae 100644 --- a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts +++ b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { createGraphViewProviderPhysicsSettingsMethods } from '../../../../src/extension/graphView/provider/physicsSettings'; describe('graphView/provider/physicsSettings', () => { diff --git a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts index 62e9b31d4..a64f29854 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts @@ -4,7 +4,7 @@ import path from 'path'; import * as vscode from 'vscode'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { createGraphViewProviderSettingsStateMethods, type GraphViewProviderSettingsStateMethodDependencies, diff --git a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts index a43e76237..ea97b5d57 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { WorkspaceFolder } from 'vscode'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; const mocks = vi.hoisted(() => { let workspaceFolders: WorkspaceFolder[] | undefined = undefined; diff --git a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts index c02a3feef..87dd6f8bb 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import type * as vscode from 'vscode'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { readGraphViewPhysicsSettings } from '../../../../../src/extension/graphView/settings/physics/reader'; const defaults: GraphPhysicsSettings = { diff --git a/packages/extension/tests/extension/graphView/settings/reader.test.ts b/packages/extension/tests/extension/graphView/settings/reader.test.ts index 9a90189ba..df2aa8e74 100644 --- a/packages/extension/tests/extension/graphView/settings/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import * as vscode from 'vscode'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { getGraphViewConfigTarget, normalizeDirectionColor, diff --git a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts index 6489fced1..8d6a0c366 100644 --- a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts +++ b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts @@ -4,7 +4,7 @@ import { buildGraphViewSettingsMessages, } from '../../../../src/extension/graphView/settings/messages'; import { captureGraphViewSettingsSnapshot } from '../../../../src/extension/graphView/settings/snapshot'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES } from '../../../../src/shared/settings/defaults'; function createConfig(values: Record) { diff --git a/packages/extension/tests/extension/repoSettings/defaults.test.ts b/packages/extension/tests/extension/repoSettings/defaults.test.ts index 497217d2b..636ceb9ab 100644 --- a/packages/extension/tests/extension/repoSettings/defaults.test.ts +++ b/packages/extension/tests/extension/repoSettings/defaults.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { CODEGRAPHY_MARKDOWN_PLUGIN_ID } from '@codegraphy-dev/core'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES } from '../../../src/shared/settings/defaults'; import { createDefaultEdgeVisibility, diff --git a/packages/extension/tests/playwright/graphVisuals.package-resolution.ts b/packages/extension/tests/playwright/graphRenderer.package-resolution.ts similarity index 85% rename from packages/extension/tests/playwright/graphVisuals.package-resolution.ts rename to packages/extension/tests/playwright/graphRenderer.package-resolution.ts index 08e16e115..ce0bc5673 100644 --- a/packages/extension/tests/playwright/graphVisuals.package-resolution.ts +++ b/packages/extension/tests/playwright/graphRenderer.package-resolution.ts @@ -6,14 +6,14 @@ import { MAX_NODE_SIZE, MIN_NODE_SIZE, computeConnectionSizes, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; /** * Keeps the workspace package surface under the extension's NodeNext resolution. * The Playwright typecheck imports this file even when the package's own Bundler * resolution accepts a source export that NodeNext cannot follow. */ -export const graphVisualsPackageResolutionProbe = { +export const graphRendererPackageResolutionProbe = { colors: [DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_COLOR], connectionSizes: computeConnectionSizes([], []), sizes: [MIN_NODE_SIZE, DEFAULT_NODE_SIZE, MAX_NODE_SIZE], diff --git a/packages/extension/tests/shared/fileColors.test.ts b/packages/extension/tests/shared/fileColors.test.ts index cd8ac9e82..86b2d8978 100644 --- a/packages/extension/tests/shared/fileColors.test.ts +++ b/packages/extension/tests/shared/fileColors.test.ts @@ -5,7 +5,7 @@ import { DEFAULT_DIRECTION_COLOR, FILE_TYPE_COLORS, getFileColor, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { normalizeHexColor } from '../../src/shared/normalizeHexColor'; describe('normalizeHexColor', () => { diff --git a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts index f2ffbcf8b..248c32bab 100644 --- a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts +++ b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { CORE_GRAPH_NODE_TYPES, createCoreGraphNodeTypes, diff --git a/packages/extension/tests/shared/mockData.test.ts b/packages/extension/tests/shared/mockData.test.ts index 5f2fb7f79..cadf26136 100644 --- a/packages/extension/tests/shared/mockData.test.ts +++ b/packages/extension/tests/shared/mockData.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { getMockGraphData, MOCK_FILE_DATA } from '../../src/shared/mockData'; -import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('Mock Data', () => { describe('MOCK_FILE_DATA', () => { diff --git a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx index e45b5ddf1..1da258e3d 100644 --- a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx index 1ad8eec8c..29dfb59df 100644 --- a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx index c9da86bdb..956d3042c 100644 --- a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx index 5764628c0..dc3418319 100644 --- a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx index fe1f4bc40..19e71d22d 100644 --- a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx +++ b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/view/fixture.ts b/packages/extension/tests/webview/app/shell/view/fixture.ts index e5bcc3501..2ca96e836 100644 --- a/packages/extension/tests/webview/app/shell/view/fixture.ts +++ b/packages/extension/tests/webview/app/shell/view/fixture.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; export const messageListeners: Array<(event: MessageEvent) => void> = []; diff --git a/packages/extension/tests/webview/export/shared/context.test.ts b/packages/extension/tests/webview/export/shared/context.test.ts index af3b93238..9adeadcb1 100644 --- a/packages/extension/tests/webview/export/shared/context.test.ts +++ b/packages/extension/tests/webview/export/shared/context.test.ts @@ -4,7 +4,7 @@ import { createImageExportDataUrl, resolveDirectionColor, } from '../../../../src/webview/export/shared/context'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; afterEach(() => { vi.restoreAllMocks(); diff --git a/packages/extension/tests/webview/export/svg/link/document.test.ts b/packages/extension/tests/webview/export/svg/link/document.test.ts index 8b273e0a5..c48bec8ee 100644 --- a/packages/extension/tests/webview/export/svg/link/document.test.ts +++ b/packages/extension/tests/webview/export/svg/link/document.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { assembleSvg, createBaseParts, createDefinitions, getPalette } from '../../../../../src/webview/export/svg/link/document'; import type { SvgExportOptions } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/element/build.test.ts b/packages/extension/tests/webview/export/svg/link/element/build.test.ts index ebf145d13..1e8e59397 100644 --- a/packages/extension/tests/webview/export/svg/link/element/build.test.ts +++ b/packages/extension/tests/webview/export/svg/link/element/build.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { buildLinkElement } from '../../../../../../src/webview/export/svg/link/element/build'; import type { SvgExportLink } from '../../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/links.test.ts b/packages/extension/tests/webview/export/svg/link/links.test.ts index 135fe8691..3ef07edf5 100644 --- a/packages/extension/tests/webview/export/svg/link/links.test.ts +++ b/packages/extension/tests/webview/export/svg/link/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { appendLinkElements } from '../../../../../src/webview/export/svg/link/links'; import type { SvgExportLink } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/graph/drag.test.tsx b/packages/extension/tests/webview/graph/drag.test.tsx index 1ebe8b033..232a82700 100644 --- a/packages/extension/tests/webview/graph/drag.test.tsx +++ b/packages/extension/tests/webview/graph/drag.test.tsx @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, act } from '@testing-library/react'; import Graph from '../../../src/webview/components/graph/view/component'; -import { toGraphPhysicsLayoutConfig } from '@codegraphy-dev/graph-visuals'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { toGraphPhysicsLayoutConfig } from '@codegraphy-dev/graph-renderer/visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import { graphStore } from '../../../src/webview/store/state'; import OwnedGraphSurface from '../../__mocks__/ownedGraphSurface'; diff --git a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx index 651069051..4a28b6e17 100644 --- a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx +++ b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest'; import OwnedGraphSurface from '../../../__mocks__/ownedGraphSurface'; import Graph from '../../../../src/webview/components/graph/view/component'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { graphStore } from '../../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/graph/model/index.test.ts b/packages/extension/tests/webview/graph/model/index.test.ts index b62a90ddf..000da164f 100644 --- a/packages/extension/tests/webview/graph/model/index.test.ts +++ b/packages/extension/tests/webview/graph/model/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { buildGraphData, diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts index a96f5f367..6c7d81b9e 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts @@ -5,7 +5,7 @@ import { getDepthSizeMultiplier, getNodeType, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('nodeDisplay (mutation targets)', () => { describe('resolveDirectionColor', () => { diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts index aa4ee73f7..0ae89f003 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts @@ -4,7 +4,7 @@ import { getDepthOpacity, getDepthSizeMultiplier, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('nodeDisplay (mutation kill tests)', () => { /** diff --git a/packages/extension/tests/webview/graph/model/node/display.test.ts b/packages/extension/tests/webview/graph/model/node/display.test.ts index 3580ac9a4..fb6cca468 100644 --- a/packages/extension/tests/webview/graph/model/node/display.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import { FAVORITE_BORDER_COLOR, getDepthOpacity, diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts index 31b078c6c..45f1d91ec 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { computeConnectionSizes } from '@codegraphy-dev/graph-visuals'; +import { computeConnectionSizes } from '@codegraphy-dev/graph-renderer/visuals'; function relatedGraph(relatedNodeCount: number) { const leaves = Array.from({ length: relatedNodeCount }, (_, index) => ({ diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts index 9f29d1b50..3f9e085e5 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts @@ -3,7 +3,7 @@ import { MIN_NODE_SIZE, MAX_NODE_SIZE, computeConnectionSizes, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('semantic node size range', () => { it('matches Obsidian’s bounded radius domain', () => { diff --git a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts index aa84ed0e9..9f70bc541 100644 --- a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts +++ b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { EdgeDecorationPayload } from '../../../../../../src/shared/plugins/decorations'; import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/components/graph/appearance/model'; import type { FGLink } from '../../../../../../src/webview/components/graph/model/build'; diff --git a/packages/extension/tests/webview/graph/rendering/links.test.ts b/packages/extension/tests/webview/graph/rendering/links.test.ts index ba59f2e56..adcbfea26 100644 --- a/packages/extension/tests/webview/graph/rendering/links.test.ts +++ b/packages/extension/tests/webview/graph/rendering/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { EdgeDecorationPayload } from '../../../../src/shared/plugins/decorations'; import type { FGLink } from '../../../../src/webview/components/graph/model/build'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../../../src/webview/components/graph/appearance/model'; diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts index bb06300e5..2b29fa9cc 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; import { diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts index 973f92b11..f6ba45e4c 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-renderer/visuals'; import type { Surface2dProps } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts'; export function createDefaultSurfaceProps(): Surface2dProps { diff --git a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts index 02a437284..4a2d5ffbd 100644 --- a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts +++ b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts @@ -3,7 +3,7 @@ import { DEFAULT_GRAPH_PHYSICS_SETTINGS, applyGraphPhysicsSettings, toGraphPhysicsLayoutConfig, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGNode } from '../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; import { createGraphLayoutEngine } from '@codegraphy-dev/graph-renderer'; diff --git a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts index 5e7f57e95..e4c25e6fd 100644 --- a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts +++ b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { FGNode } from '../../../../../src/webview/components/graph/model/build'; -import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import { getTooltipNodeRect } from '../../../../../src/webview/components/graph/runtime/tooltip/rect'; describe('getTooltipNodeRect', () => { diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts index 1ed39c290..7cf7fa05a 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import { FAVORITE_BORDER_COLOR } from '../../../../../../src/webview/components/graph/model/build'; import { applyNodeAppearance } from '../../../../../../src/webview/components/graph/runtime/use/indicators/nodeAppearance'; import { adjustColorForLightTheme } from '../../../../../../src/webview/theme/useTheme'; diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts index 85518e1a0..7c831df70 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts @@ -3,7 +3,7 @@ import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/componen import { type FGNode, } from '../../../../../../src/webview/components/graph/model/build'; -import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; export const DARK_APPEARANCE = { ...DEFAULT_GRAPH_APPEARANCE, diff --git a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx index 7a4aee1ce..aaeaaa2db 100644 --- a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx +++ b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { GraphViewStoreState } from '../../../../src/webview/components/graph/view/store'; import type { UseGraphInteractionRuntimeResult } from '../../../../src/webview/components/graph/runtime/use/interaction'; import type { GraphRuntime } from '../../../../src/webview/components/graph/runtime/use/state'; diff --git a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts index 5ef0032e2..7057a1fed 100644 --- a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts +++ b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '@codegraphy-dev/graph-visuals'; +} from '@codegraphy-dev/graph-renderer/visuals'; function node(id: string, nodeType?: string, color = ''): IGraphNode { return { @@ -105,7 +105,7 @@ describe('webview/graphControls/filtering nodes', () => { it('uses node-type-specific fallback colors when no color is configured', async () => { vi.resetModules(); - vi.doMock('@codegraphy-dev/graph-visuals', () => ({ + vi.doMock('@codegraphy-dev/graph-renderer/visuals', () => ({ DEFAULT_FOLDER_NODE_COLOR: '#folder', DEFAULT_NODE_COLOR: '#file', DEFAULT_PACKAGE_NODE_COLOR: '#package', @@ -126,7 +126,7 @@ describe('webview/graphControls/filtering nodes', () => { { ...node('a.ts'), color: '#file' }, ]); } finally { - vi.doUnmock('@codegraphy-dev/graph-visuals'); + vi.doUnmock('@codegraphy-dev/graph-renderer/visuals'); vi.resetModules(); } }); diff --git a/packages/extension/tests/webview/search/filtering.test.ts b/packages/extension/tests/webview/search/filtering.test.ts index eb873be03..d61a74dc5 100644 --- a/packages/extension/tests/webview/search/filtering.test.ts +++ b/packages/extension/tests/webview/search/filtering.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import type { IGroup } from '../../../src/shared/settings/groups'; import { diff --git a/packages/extension/tests/webview/search/filtering/rules.test.ts b/packages/extension/tests/webview/search/filtering/rules.test.ts index 9f718d13e..54888c632 100644 --- a/packages/extension/tests/webview/search/filtering/rules.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import type { IGroup } from '../../../../src/shared/settings/groups'; import { applyLegendRules } from '../../../../src/webview/search/filtering/rules'; diff --git a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts index 8bab2193e..0766dd689 100644 --- a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { applyNodeLegendRules, compileNodeLegendRules, diff --git a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx index b4a85fc1e..bb8951b99 100644 --- a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import SettingsPanel from '../../../src/webview/components/settingsPanel/Drawer'; import { graphStore } from '../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx index 6c6bd6cd2..86c6798c8 100644 --- a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ForcesSection } from '../../../../src/webview/components/settingsPanel/forces/Section'; import { graphStore } from '../../../../src/webview/store/state'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({ Slider: ({ diff --git a/packages/extension/tests/webview/settingsPanel/forces/persistence.test.ts b/packages/extension/tests/webview/settingsPanel/forces/persistence.test.ts index d3722457a..99a02b60f 100644 --- a/packages/extension/tests/webview/settingsPanel/forces/persistence.test.ts +++ b/packages/extension/tests/webview/settingsPanel/forces/persistence.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-visuals'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { clearPhysicsTimerMap, flushPendingPhysicsValue, diff --git a/packages/extension/tests/webview/store/state/defaults.test.ts b/packages/extension/tests/webview/store/state/defaults.test.ts index 7914287f1..57ad79d0b 100644 --- a/packages/extension/tests/webview/store/state/defaults.test.ts +++ b/packages/extension/tests/webview/store/state/defaults.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES } from '../../../../src/shared/settings/defaults'; import { createGraphStore } from '../../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts b/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts index c19bce822..a4f6ed463 100644 --- a/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts +++ b/packages/extension/tests/webview/store/state/runtime/graphMessages.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { createGraphStore } from '../../../../../src/webview/store/state'; -import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { clearSentMessages } from '../../../../helpers/sentMessages'; describe('GraphStore graph messages',()=>{ diff --git a/packages/graph-renderer/README.md b/packages/graph-renderer/README.md index e8e8c46c8..e3a24eeac 100644 --- a/packages/graph-renderer/README.md +++ b/packages/graph-renderer/README.md @@ -4,9 +4,10 @@ A browser graph engine for large, interactive force-directed graphs. It combines - WebGPU rendering for nodes, links, curves, self-links, and directional arrows. - A fast deterministic WebAssembly force simulation with Barnes–Hut charge, links, centering, collision, pinning, and hidden-node support. +- Shared Node geometry, Material Icon Theme matching, connection sizing, and force-control semantics for CodeGraphy interfaces. - Typed-array data paths that keep graph indexes stable across physics, rendering, picking, and application state. -The package owns graph rendering and graph physics only. Your application owns its graph model, labels, controls, interactions, picking policy, settings, persistence, tooltips, and UI. That boundary keeps the engine usable outside CodeGraphy. +The package owns graph rendering, graph physics, and reusable visual semantics. Applications own their graph model, host theme resolution, interactions, settings persistence, tooltips, and UI. ## Install @@ -188,11 +189,13 @@ There is intentionally no Canvas 2D or WebGL fallback in this package. A consumi | `GraphRenderer*` types | Renderer nodes, links, styles, cameras, and frames. | | `GraphNodeFlag` | Mark nodes as pinned or hidden. | | Geometry/visibility helpers | Keep application picking and detail UI aligned with rendering. | +| Material and visual helpers | Keep icons, Node sizing, colors, and force controls consistent across interfaces. | -The supported consumer entry point is the package root: +The supported runtime entry point is the package root. Reusable CodeGraphy visual semantics use the `visuals` subpath of the same package: ```ts import { createGraphLayoutEngine, WebGpuGraphRenderer } from '@codegraphy-dev/graph-renderer'; +import { findMaterialMatch } from '@codegraphy-dev/graph-renderer/visuals'; ``` Internal WebGPU buffers, shaders, WASM storage, and test hooks are not public API. diff --git a/packages/graph-renderer/build.mjs b/packages/graph-renderer/build.mjs index b5404b733..5e877a98c 100644 --- a/packages/graph-renderer/build.mjs +++ b/packages/graph-renderer/build.mjs @@ -24,8 +24,11 @@ execFileSync( ); await build({ - entryPoints: [path.join(packageRoot, 'src/index.ts')], - outfile: path.join(distRoot, 'index.js'), + entryPoints: { + index: path.join(packageRoot, 'src/index.ts'), + 'visuals/index': path.join(packageRoot, 'src/visuals/index.ts'), + }, + outdir: distRoot, bundle: true, format: 'esm', platform: 'node', diff --git a/packages/graph-renderer/package.json b/packages/graph-renderer/package.json index 91d0f1fb9..4c610865c 100644 --- a/packages/graph-renderer/package.json +++ b/packages/graph-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@codegraphy-dev/graph-renderer", "version": "0.4.0", - "description": "CodeGraphy's WebGPU graph renderer and deterministic WebAssembly physics engine", + "description": "CodeGraphy's shared graph visuals, WebGPU renderer, and deterministic WebAssembly physics engine", "license": "MIT", "type": "module", "sideEffects": false, @@ -12,6 +12,10 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./visuals": { + "types": "./src/visuals/index.ts", + "default": "./src/visuals/index.ts" + }, "./testing": { "types": "./tests/api.ts", "default": "./tests/api.ts" @@ -48,6 +52,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./visuals": { + "types": "./dist/visuals/index.d.ts", + "default": "./dist/visuals/index.js" + }, "./physics.wasm": "./dist/generated/physics.wasm" } }, diff --git a/packages/graph-visuals/src/graph.ts b/packages/graph-renderer/src/visuals/graph.ts similarity index 100% rename from packages/graph-visuals/src/graph.ts rename to packages/graph-renderer/src/visuals/graph.ts diff --git a/packages/graph-visuals/src/index.ts b/packages/graph-renderer/src/visuals/index.ts similarity index 100% rename from packages/graph-visuals/src/index.ts rename to packages/graph-renderer/src/visuals/index.ts diff --git a/packages/graph-visuals/src/materialTheme.ts b/packages/graph-renderer/src/visuals/materialTheme.ts similarity index 100% rename from packages/graph-visuals/src/materialTheme.ts rename to packages/graph-renderer/src/visuals/materialTheme.ts diff --git a/packages/graph-visuals/src/physics.ts b/packages/graph-renderer/src/visuals/physics.ts similarity index 100% rename from packages/graph-visuals/src/physics.ts rename to packages/graph-renderer/src/visuals/physics.ts diff --git a/packages/graph-visuals/tests/index.test.ts b/packages/graph-renderer/tests/visuals/index.test.ts similarity index 98% rename from packages/graph-visuals/tests/index.test.ts rename to packages/graph-renderer/tests/visuals/index.test.ts index eb64ca468..3c7a46298 100644 --- a/packages/graph-visuals/tests/index.test.ts +++ b/packages/graph-renderer/tests/visuals/index.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment node + import { Buffer } from 'node:buffer'; import { describe, expect, it } from 'vitest'; import { @@ -30,7 +32,7 @@ import { getFileColor, toSvgDataUrl, toWhiteSvgDataUrl, -} from '../src/index'; +} from '../../src/visuals/index.js'; describe('graph visual semantics', () => { it('exports the shared palette and rendering metrics', () => { diff --git a/packages/graph-visuals/tests/physics.test.ts b/packages/graph-renderer/tests/visuals/physics.test.ts similarity index 97% rename from packages/graph-visuals/tests/physics.test.ts rename to packages/graph-renderer/tests/visuals/physics.test.ts index 1bd8f66c7..80bb363ab 100644 --- a/packages/graph-visuals/tests/physics.test.ts +++ b/packages/graph-renderer/tests/visuals/physics.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment node + import { describe, expect, it, vi } from 'vitest'; import { DEFAULT_GRAPH_PHYSICS_SETTINGS, @@ -7,7 +9,7 @@ import { normalizeGraphPhysicsSettings, toGraphPhysicsLayoutConfig, type GraphPhysicsSettings, -} from '../src/index.js'; +} from '../../src/visuals/index.js'; describe('graph physics contract', () => { it('defines the shared defaults and visible control limits', () => { diff --git a/packages/graph-visuals/package.json b/packages/graph-visuals/package.json deleted file mode 100644 index e545c24d0..000000000 --- a/packages/graph-visuals/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@codegraphy-dev/graph-visuals", - "version": "0.1.0", - "private": true, - "description": "Shared visual semantics for CodeGraphy graph interfaces", - "license": "MIT", - "type": "module", - "sideEffects": false, - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - }, - "scripts": { - "test": "vitest run --config vitest.config.ts", - "typecheck": "tsc --noEmit -p tsconfig.json" - }, - "devDependencies": { - "typescript": "^5.3.3", - "vitest": "^3.2.4" - } -} diff --git a/packages/graph-visuals/tsconfig.json b/packages/graph-visuals/tsconfig.json deleted file mode 100644 index 89e54cfb3..000000000 --- a/packages/graph-visuals/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM"], - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "noEmit": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "skipLibCheck": true, - "types": ["vitest/globals"] - }, - "include": ["src", "tests", "vitest.config.ts"] -} diff --git a/packages/graph-visuals/vitest.config.ts b/packages/graph-visuals/vitest.config.ts deleted file mode 100644 index 695b38ab7..000000000 --- a/packages/graph-visuals/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - root: import.meta.dirname, - test: { - environment: 'node', - include: ['tests/**/*.test.ts'], - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2643ea50..8965281a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,6 @@ importers: '@codegraphy-dev/graph-renderer': specifier: workspace:* version: link:../../packages/graph-renderer - '@codegraphy-dev/graph-visuals': - specifier: workspace:* - version: link:../../packages/graph-visuals '@codemirror/commands': specifier: ^6.10.4 version: 6.10.4 @@ -358,9 +355,6 @@ importers: '@codegraphy-dev/graph-renderer': specifier: workspace:* version: link:../graph-renderer - '@codegraphy-dev/graph-visuals': - specifier: workspace:* - version: link:../graph-visuals '@codegraphy-dev/plugin-api': specifier: workspace:* version: link:../plugin-api @@ -579,15 +573,6 @@ importers: specifier: ^5.3.3 version: 5.9.3 - packages/graph-visuals: - devDependencies: - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) - packages/plugin-api: {} packages/plugin-godot: From d0b6ec2e82c9d9169d13fc3572022a136dc84786 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 09:53:07 -0700 Subject: [PATCH 23/25] perf(desktop): warm Core before workspace open --- apps/desktop/src-tauri/src/lib.rs | 8 ++++++++ apps/desktop/src/App.tsx | 2 +- apps/desktop/src/useDesktopWorkspace.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 4048ae48d..699137a06 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -783,6 +783,14 @@ pub fn run() { .map_err(std::io::Error::other)?; refresh_app_menu(app.handle(), &recents).map_err(std::io::Error::other)?; app.manage(recents); + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let core = app_handle.state::(); + let mut state = core.inner.lock().await; + if state.process.is_none() { + state.process = spawn_core_process(&app_handle).ok(); + } + }); Ok(()) }) .on_menu_event(|app, event| handle_menu_event(app, event.id().as_ref())) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 4247421f7..e6ef714b8 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -193,7 +193,7 @@ export function App(): React.ReactElement { {interfacePreferences.profilingVisible ? ( {workspace.graph ? `${formatGraphCounts(workspace.graph)} · ` : ''} {workspace.metricsSummary ?? 'Switch Files to record open latency'} diff --git a/apps/desktop/src/useDesktopWorkspace.ts b/apps/desktop/src/useDesktopWorkspace.ts index 0e5ae5519..766fdcbc1 100644 --- a/apps/desktop/src/useDesktopWorkspace.ts +++ b/apps/desktop/src/useDesktopWorkspace.ts @@ -185,7 +185,7 @@ function formatFileSwitchMetrics(metrics: FileSwitchMetrics | undefined): string const last = metrics.samples.at(-1) ?? 0; const median = percentile(metrics.samples, 0.5); const p95 = percentile(metrics.samples, 0.95); - return `File open ${last.toFixed(1)} ms · median ${median.toFixed(1)} · p95 ${p95.toFixed(1)} · n=${metrics.samples.length}`; + return `File request ${last.toFixed(1)} ms · median ${median.toFixed(1)} · p95 ${p95.toFixed(1)} · n=${metrics.samples.length}`; } export function useDesktopWorkspace() { From 737a1914a31d6504e0cca8df96922b0dcbce9f65 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 10:13:27 -0700 Subject: [PATCH 24/25] docs(desktop): document bounded navigation timing --- apps/desktop/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 28c6bda6f..e0a21eb67 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -17,6 +17,7 @@ Intel is not a supported release target yet. The graph renderer has no non-WebGP - Open any local folder as a CodeGraphy Workspace. - Switch workspaces from the toolbar or the native `File > Open Recent` menu. Missing recent folders stay visible as unavailable until the user clears the menu. - Browse and filter a thin semantic File and Folder hierarchy. Arrow keys, Home, End, Enter, Space, type-ahead, `/`, and `Cmd+F` work while the hierarchy has focus. Up, Down, Home, End, and Right open each focused File without moving focus into the editor, so keyboard switching stays continuous. +- Keep rapid hierarchy navigation bounded to one active File request and one replaceable latest request. Repeated keys do not queue obsolete File reads or scroll frames after key release. - Open UTF-8 text Files up to 5 MiB in CodeMirror with the maintained One Dark highlight theme and maintained language support for common source and data formats. Close File clears only the editor after the same dirty-edit confirmation used for switching Files. - Edit Markdown as source, show a live rendered preview, or use a split source-and-preview view. Markdown preview code loads only for Markdown Files and stays independent of the Core plugin system. - Save through an atomic replacement that preserves permissions and rejects an external edit conflict. @@ -27,14 +28,14 @@ Intel is not a supported release target yet. The graph renderer has no non-WebGP - Read the live ` Nodes · Relationships` count for the displayed File and Folder graph. - Resize all three panes with pointer or keyboard separators. Pane proportions persist in local interface state and clamp to usable widths when the window changes. - Hide the File hierarchy or Relationship Graph independently to focus on the open File. These interface preferences stay local and restore on the next launch. -- Show measured File-open latency only when Profile is enabled. The measurement starts before the Rust File read and ends after React, CodeMirror, and two painted frames; it reports the latest, median, and p95 sample instead of inferring speed from animation. +- Show measured File-request latency only when Profile is enabled. The measurement starts before the Rust request and ends after two animation frames; it reports the latest, median, and p95 sample without claiming a CodeMirror-ready or GPU-frame-complete timestamp. - Tune Repel Force, Center Force, and Link Distance from 30 to 150 live from Graph Settings. Reset restores the extension defaults. The desktop record persists in the workspace without restarting Core or Indexing. Source Files and the Graph Cache stay in the workspace. The app does not upload them. ## Architecture -The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. +The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. Tauri starts the sidecar during app setup so the first workspace does not also pay the process and Core import cost. Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-renderer` owns WebGPU drawing, WebAssembly physics, Material icon matching, Node appearance, connection sizing, and the named force-control contract shared with the extension. Desktop pointer handling, host theme resolution, and settings persistence stay in the desktop interface. From 4646c1b98bb1a5e926ef795ed8eded34a7b8e3a1 Mon Sep 17 00:00:00 2001 From: joesobo Date: Fri, 7 Aug 2026 10:25:56 -0700 Subject: [PATCH 25/25] fix(graph-renderer): support extension E2E resolution --- packages/extension/package.json | 2 +- packages/graph-renderer/package.json | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index cbd489ed6..2b6cf9550 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -27,7 +27,7 @@ "test:webview-browser:minimap": "pnpm run build:webview && playwright test --config playwright.config.ts tests/playwright/depth-view.spec.ts --grep \"keeps the minimap viewport aligned\"", "test:vscode": "pnpm run test:playwright", "lint": "pnpm run generate:acceptance && eslint \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"tests/**/*.{ts,tsx}\"", - "typecheck": "pnpm run generate:acceptance && tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.tests.json && pnpm -w exec turbo run build --filter=@codegraphy-dev/core... && tsc --noEmit -p tests/playwright/tsconfig.json" + "typecheck": "pnpm run generate:acceptance && tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.tests.json && tsc --noEmit -p src/e2e/tsconfig.json && pnpm -w exec turbo run build --filter=@codegraphy-dev/core... && tsc --noEmit -p tests/playwright/tsconfig.json" }, "dependencies": { "@codegraphy-dev/core": "workspace:*", diff --git a/packages/graph-renderer/package.json b/packages/graph-renderer/package.json index 4c610865c..0c4308772 100644 --- a/packages/graph-renderer/package.json +++ b/packages/graph-renderer/package.json @@ -22,6 +22,13 @@ }, "./physics.wasm": "./src/physics/wasm/generated/physics.wasm" }, + "typesVersions": { + "*": { + "visuals": [ + "src/visuals/index.ts" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/joesobo/CodeGraphyV4.git" @@ -57,6 +64,13 @@ "default": "./dist/visuals/index.js" }, "./physics.wasm": "./dist/generated/physics.wasm" + }, + "typesVersions": { + "*": { + "visuals": [ + "dist/visuals/index.d.ts" + ] + } } }, "files": [