diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b0e644e..2930044 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -38,18 +38,49 @@ The entire extension's logic is contained within the `CopilotTokenTracker` class ## Logging Best Practices -**CRITICAL**: Do NOT add debug logging statements like `log('[DEBUG] message')` for troubleshooting during development. This approach has been found to interfere with the output channel and can hide existing log messages from appearing. +**CRITICAL**: Do NOT add debug logging statements like `this.log('[DEBUG] message')` or `console.log('[DEBUG] ...')` for troubleshooting during development. This approach has been found to flood the output channel and cause messages to disappear. -- **Use Existing Logs**: The extension already has comprehensive logging throughout. Review existing log statements to understand what's being tracked. -- **Minimal Logging**: Only add logging if absolutely necessary for a new feature. Keep messages concise and informative. -- **Remove Debug Logs**: Any temporary debug logging added during development MUST be removed before committing code. -- **Log Methods**: Use the appropriate method for the severity: +### Why DEBUG Logs Are Problematic + +**Extension Host Flooding**: When DEBUG log statements are added to frequently-called methods in `extension.ts` (e.g., cache lookups, file processing loops, webview message handlers), they can generate hundreds of log entries per operation. VS Code's OutputChannel has a buffer limit, and excessive logging causes older messages to be pushed out and lost. This was observed when: +- Cache hit/miss logging was added to session file processing +- JSONL content reference counting was logged for each file +- Webview message handlers logged every incoming message with full JSON payloads +- Session data was logged with repository counts on each webview update + +**Symptom**: After operations like clearing the cache, expected log messages would disappear from the Output panel because they were pushed out of the buffer by DEBUG logs. + +### Extension vs Webview Logging + +These are two completely separate logging systems: + +| Context | Method | Destination | Visibility | +|---------|--------|-------------|------------| +| Extension (`src/extension.ts`) | `this.log()`, `this.warn()`, `this.error()` | VS Code Output Channel | Output panel → "Copilot Token Tracker" | +| Webview (`src/webview/*/main.ts`) | `console.log()` | Browser DevTools | Help → Toggle Developer Tools in webview | + +- Clearing the output channel (`outputChannel.clear()`) does NOT affect webview console logs +- Webview console.log statements do NOT appear in the Output panel +- DEBUG prefixes in webviews were removed to maintain consistency with extension guidelines + +### Best Practices + +- **Use Existing Logs**: The extension already has comprehensive logging. Review existing log statements before adding new ones. +- **Minimal Logging**: Only add logging if absolutely necessary for a new feature. Keep messages concise. +- **Remove Debug Logs**: Any temporary debug logging added during development MUST be removed before committing. +- **Log Methods**: Use appropriate severity: - `log(message)` - Standard informational messages - `warn(message)` - Warnings or recoverable errors - `error(message)` - Critical errors -- **No Debug Prefixes**: Avoid prefixing messages with `[DEBUG]` or similar markers. The log output is already timestamped and categorized. +- **No Debug Prefixes**: Avoid `[DEBUG]` markers. The log output is already timestamped. +- **Avoid High-Frequency Logging**: Never log inside loops that process many items (files, sessions, cache entries). + +### Debugging Without Logs -If you need to troubleshoot execution flow, prefer using VS Code's debugger with breakpoints rather than adding log statements. +Prefer VS Code's debugger with breakpoints rather than adding log statements: +1. Press `F5` to launch Extension Development Host +2. Set breakpoints in `src/extension.ts` +3. Use the Debug Console to inspect variables ## Key Files & Conventions diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..1bbdb38 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["stylelint-config-standard"], + "rules": { + "selector-class-pattern": null, + "custom-property-pattern": null, + "no-descending-specificity": null, + "color-function-notation": null, + "alpha-value-notation": null + } +} diff --git a/esbuild.js b/esbuild.js index 21d0b03..2b42477 100644 --- a/esbuild.js +++ b/esbuild.js @@ -59,6 +59,7 @@ async function main() { external: ['vscode'], logLevel: 'silent', plugins: [esbuildProblemMatcherPlugin], + loader: { '.css': 'text' }, }); if (watch) { diff --git a/package-lock.json b/package-lock.json index 149eaf5..481bb71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,8 @@ "esbuild": "^0.27.2", "eslint": "^9.39.2", "npm-run-all": "^4.1.5", + "stylelint": "^17.1.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.9.3" }, "engines": { @@ -479,6 +481,67 @@ "node": ">=18" } }, + "node_modules/@cacheable/memory": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz", + "integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.3.3", + "@keyv/bigmap": "^1.3.0", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz", + "integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.3.0", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -571,9 +634,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.22.tgz", - "integrity": "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", "funding": [ { "type": "github", @@ -584,10 +647,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } + "license": "MIT-0" }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", @@ -608,6 +668,52 @@ "node": ">=18" } }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", + "integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -1387,6 +1493,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -2969,6 +3082,30 @@ } } }, + "node_modules/cacheable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz", + "integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.7", + "@cacheable/utils": "^2.3.3", + "hookified": "^1.15.0", + "keyv": "^5.5.5", + "qified": "^0.6.0" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3297,6 +3434,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3330,6 +3474,52 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3345,6 +3535,16 @@ "node": ">= 8" } }, + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12 || >=16" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -3388,6 +3588,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "5.3.5", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.5.tgz", @@ -3807,6 +4020,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -4368,6 +4591,16 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4711,6 +4944,47 @@ "node": ">= 6" } }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4773,6 +5047,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4874,6 +5155,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", + "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4897,6 +5191,13 @@ "he": "bin/he" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4929,6 +5230,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html-tags": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-5.1.0.tgz", + "integrity": "sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -5057,6 +5371,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5091,8 +5416,7 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -5449,6 +5773,16 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -5822,6 +6156,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5943,6 +6284,23 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5977,6 +6335,13 @@ "immediate": "~3.0.5" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -6114,6 +6479,17 @@ "node": ">= 0.4" } }, + "node_modules/mathml-tag-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz", + "integrity": "sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -6129,6 +6505,19 @@ "node": ">= 0.10.0" } }, + "node_modules/meow": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.0.0.tgz", + "integrity": "sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6355,10 +6744,29 @@ "dev": true, "license": "ISC" }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT", "optional": true @@ -7172,6 +7580,83 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -7247,6 +7732,19 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", + "integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -8348,6 +8846,324 @@ "boundary": "^2.0.0" } }, + "node_modules/stylelint": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.1.1.tgz", + "integrity": "sha512-SBHVcLEcRF1M9OkD3oT0hT2PayDNLw2hd+aovmzfNQ2ys4Xd3oS9ZNizILWqhQvW802AqKN/vUTMwJqQYMBlWw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.0.25", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0", + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "balanced-match": "^3.0.1", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.2", + "global-modules": "^2.0.0", + "globby": "^16.1.0", + "globjoin": "^0.1.4", + "html-tags": "^5.1.0", + "ignore": "^7.0.5", + "import-meta-resolve": "^4.2.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^4.0.0", + "meow": "^14.0.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0", + "string-width": "^8.1.0", + "supports-hyperlinks": "^4.4.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^7.0.0" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-18.0.0.tgz", + "integrity": "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "stylelint": "^17.0.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "40.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-40.0.0.tgz", + "integrity": "sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^18.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "stylelint": "^17.0.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/stylelint/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", + "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", + "integrity": "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.20" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.20.tgz", + "integrity": "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.2", + "flatted": "^3.3.3", + "hookified": "^1.15.0" + } + }, + "node_modules/stylelint/node_modules/globby": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.1.0.tgz", + "integrity": "sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/supports-hyperlinks": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz", + "integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -8402,6 +9218,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -9313,6 +10135,20 @@ "license": "ISC", "optional": true }, + "node_modules/write-file-atomic": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.0.tgz", + "integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index 9a8a26c..4336ab5 100644 --- a/package.json +++ b/package.json @@ -233,6 +233,7 @@ "pretest": "npm run compile && npm run compile-tests && npm run lint", "check-types": "tsc --noEmit", "lint": "eslint src", + "lint:css": "stylelint \"src/webview/**/*.css\"", "test": "vscode-test", "test:node": "npm run compile-tests && node --test out/test/test-node/backend-identity.test.js", "test:coverage": "npm run compile-tests && node --require ./out/test/test-node/vscode-shim-register.js --experimental-test-coverage --test --test-coverage-lines=60 --test-coverage-functions=60 --test-coverage-branches=60 --test-coverage-include=out/test/backend/**/*.js --test-coverage-include=out/test/utils/**/*.js out/test/test-node/backend-identity.test.js out/test/test-node/utils-errors.test.js out/test/test-node/backend-settings.test.js out/test/test-node/backend-copyConfig.test.js out/test/test-node/backend-integration.test.js out/test/test-node/backend-commands.test.js out/test/test-node/backend-facade-helpers.test.js out/test/test-node/backend-facade-rollups.test.js out/test/test-node/backend-facade-query.test.js", @@ -251,6 +252,8 @@ "esbuild": "^0.27.2", "eslint": "^9.39.2", "npm-run-all": "^4.1.5", + "stylelint": "^17.1.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.9.3" }, "dependencies": { diff --git a/src/extension.ts b/src/extension.ts index 51c916e..fec91b6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1546,84 +1546,97 @@ class CopilotTokenTracker implements vscode.Disposable { // Handle .jsonl files OR .json files with JSONL content (Copilot CLI format and VS Code incremental format) const isJsonlContent = sessionFile.endsWith('.jsonl') || this.isJsonlContent(fileContent); if (isJsonlContent) { - const lines = fileContent.trim().split('\n'); - let sessionMode = 'ask'; // Default mode + const lines = fileContent.trim().split('\n').filter(l => l.trim()); - for (const line of lines) { - if (!line.trim()) { continue; } + // Detect if this is delta-based format (VS Code incremental) + let isDeltaBased = false; + if (lines.length > 0) { try { - const event = JSON.parse(line); - - // Handle VS Code incremental format - detect mode from session header - if (event.kind === 0 && event.v?.inputState?.mode?.kind) { - sessionMode = event.v.inputState.mode.kind; - - // Detect implicit selections in initial state - if (event.v?.inputState?.selections && Array.isArray(event.v.inputState.selections) && event.v.inputState.selections.length > 0) { + const firstLine = JSON.parse(lines[0]); + if (firstLine && typeof firstLine.kind === 'number') { + isDeltaBased = true; + } + } catch { + // Not delta format + } + } + + if (isDeltaBased) { + // Delta-based format: reconstruct full state first, then process + let sessionState: any = {}; + for (const line of lines) { + try { + const delta = JSON.parse(line); + sessionState = this.applyDelta(sessionState, delta); + } catch { + // Skip invalid lines + } + } + + // Extract session mode from reconstructed state + let sessionMode = 'ask'; + if (sessionState.inputState?.mode?.kind) { + sessionMode = sessionState.inputState.mode.kind; + } + + // Detect implicit selections + if (sessionState.inputState?.selections && Array.isArray(sessionState.inputState.selections)) { + for (const sel of sessionState.inputState.selections) { + if (sel && (sel.startLineNumber !== sel.endLineNumber || sel.startColumn !== sel.endColumn)) { analysis.contextReferences.implicitSelection++; + break; } } + } + + // Process reconstructed requests array + const requests = sessionState.requests || []; + for (const request of requests) { + if (!request || !request.requestId) { continue; } - // Handle mode changes (kind: 1 with mode update) - if (event.kind === 1 && event.k?.includes('mode') && event.v?.kind) { - sessionMode = event.v.kind; + // Count by mode + if (sessionMode === 'agent') { + analysis.modeUsage.agent++; + } else if (sessionMode === 'edit') { + analysis.modeUsage.edit++; + } else { + analysis.modeUsage.ask++; } - // Detect implicit selections in updates to inputState.selections - if (event.kind === 1 && event.k?.includes('selections') && Array.isArray(event.v) && event.v.length > 0) { - analysis.contextReferences.implicitSelection++; + // Check for agent in request + if (request.agent?.id) { + const toolName = request.agent.id; + analysis.toolCalls.total++; + analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; } - // Handle VS Code incremental format - count requests as interactions - if (event.kind === 2 && event.k?.[0] === 'requests' && Array.isArray(event.v)) { - for (const request of event.v) { - if (request.requestId) { - // Count by mode - if (sessionMode === 'agent') { - analysis.modeUsage.agent++; - } else if (sessionMode === 'edit') { - analysis.modeUsage.edit++; - } else { - analysis.modeUsage.ask++; - } - } - // Check for agent in request - if (request.agent?.id) { - const toolName = request.agent.id; + // Analyze all context references from this request + this.analyzeRequestContext(request, analysis.contextReferences); + + // Extract tool calls from request.response array + if (request.response && Array.isArray(request.response)) { + for (const responseItem of request.response) { + if (responseItem.kind === 'toolInvocationSerialized' || responseItem.kind === 'prepareToolInvocation') { analysis.toolCalls.total++; + const toolName = responseItem.toolId || responseItem.toolName || responseItem.invocationMessage?.toolName || responseItem.toolSpecificData?.kind || 'unknown'; analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; } - - // Analyze contentReferences if present - if (request.contentReferences && Array.isArray(request.contentReferences)) { - this.analyzeContentReferences(request.contentReferences, analysis.contextReferences); - } - - // Extract tool calls from request.response array (when full request is added) - if (request.response && Array.isArray(request.response)) { - for (const responseItem of request.response) { - if (responseItem.kind === 'toolInvocationSerialized' || responseItem.kind === 'prepareToolInvocation') { - analysis.toolCalls.total++; - const toolName = responseItem.toolId || responseItem.toolName || responseItem.invocationMessage?.toolName || responseItem.toolSpecificData?.kind || 'unknown'; - analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; - } - } - } - } - } - - // Handle VS Code incremental format - tool invocations in responses - if (event.kind === 2 && event.k?.includes('response') && Array.isArray(event.v)) { - for (const responseItem of event.v) { - if (responseItem.kind === 'toolInvocationSerialized') { - analysis.toolCalls.total++; - const toolName = responseItem.toolId || responseItem.toolName || responseItem.invocationMessage?.toolName || responseItem.toolSpecificData?.kind || 'unknown'; - analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; } } } - // Handle Copilot CLI format + // Calculate model switching for delta-based JSONL files + await this.calculateModelSwitching(sessionFile, analysis); + return analysis; + } + + // Non-delta JSONL (Copilot CLI format) - process line-by-line + for (const line of lines) { + if (!line.trim()) { continue; } + try { + const event = JSON.parse(line); + + // Handle Copilot CLI format // Detect mode from event type - CLI can be chat or agent mode if (event.type === 'user.message') { analysis.modeUsage.ask++; @@ -1704,52 +1717,8 @@ class CopilotTokenTracker implements vscode.Disposable { analysis.modeUsage.ask++; } - // Analyze user message for context references - if (request.message) { - if (request.message.text) { - this.analyzeContextReferences(request.message.text, analysis.contextReferences); - } - if (request.message.parts) { - for (const part of request.message.parts) { - if (part.text) { - this.analyzeContextReferences(part.text, analysis.contextReferences); - } - } - } - } - - // Analyze variableData for @workspace, @terminal, @vscode references - if (request.variableData) { - // Process variables array for prompt files and other context - this.analyzeVariableData(request.variableData, analysis.contextReferences); - - // Also check for @ references in variable names/values - const varDataStr = JSON.stringify(request.variableData).toLowerCase(); - if (varDataStr.includes('workspace')) { - analysis.contextReferences.workspace++; - } - if (varDataStr.includes('terminal')) { - analysis.contextReferences.terminal++; - } - if (varDataStr.includes('vscode')) { - analysis.contextReferences.vscode++; - } - } - - // Analyze contentReferences if present - if (request.contentReferences && Array.isArray(request.contentReferences)) { - this.analyzeContentReferences(request.contentReferences, analysis.contextReferences); - } - - // Analyze variableData if present - if (request.variableData) { - this.analyzeVariableData(request.variableData, analysis.contextReferences); - } - - // Analyze variableData if present - if (request.variableData) { - this.analyzeVariableData(request.variableData, analysis.contextReferences); - } + // Analyze all context references from this request + this.analyzeRequestContext(request, analysis.contextReferences); // Analyze response for tool calls and MCP tools if (request.response && Array.isArray(request.response)) { @@ -1857,6 +1826,36 @@ class CopilotTokenTracker implements vscode.Disposable { } } + /** + * Analyze a request object for all context references. + * This is the unified method that processes text, contentReferences, and variableData. + */ + private analyzeRequestContext(request: any, refs: ContextReferenceUsage): void { + // Analyze user message text for context references + if (request.message) { + if (request.message.text) { + this.analyzeContextReferences(request.message.text, refs); + } + if (request.message.parts) { + for (const part of request.message.parts) { + if (part.text) { + this.analyzeContextReferences(part.text, refs); + } + } + } + } + + // Analyze contentReferences if present + if (request.contentReferences && Array.isArray(request.contentReferences)) { + this.analyzeContentReferences(request.contentReferences, refs); + } + + // Analyze variableData if present + if (request.variableData) { + this.analyzeVariableData(request.variableData, refs); + } + } + /** * Analyze text for context references like #file, #selection, @workspace */ @@ -1873,11 +1872,16 @@ class CopilotTokenTracker implements vscode.Disposable { refs.selection += selectionMatches.length; } - // Count #symbol references + // Count #symbol and #sym references (both aliases) + // Note: #sym:symbolName format is handled via variableData, not text matching const symbolMatches = text.match(/#symbol/gi); + const symMatches = text.match(/#sym(?![:\w])/gi); // Negative lookahead: don't match #symbol or #sym: if (symbolMatches) { refs.symbol += symbolMatches.length; } + if (symMatches) { + refs.symbol += symMatches.length; + } // Count #codebase references const codebaseMatches = text.match(/#codebase/gi); @@ -1943,13 +1947,18 @@ class CopilotTokenTracker implements vscode.Disposable { // Normalize path separators for pattern matching const normalizedPath = fsPath.replace(/\\/g, '/').toLowerCase(); - // Track specific patterns + // Track specific patterns - these are auto-attached, not user-explicit #file refs if (normalizedPath.endsWith('/.github/copilot-instructions.md') || normalizedPath.includes('.github/copilot-instructions.md')) { refs.copilotInstructions++; } else if (normalizedPath.endsWith('/agents.md') || normalizedPath.match(/\/agents\.md$/i)) { refs.agentsMd++; + } else if (normalizedPath.endsWith('.instructions.md') || + normalizedPath.includes('.instructions.md')) { + // Other instruction files (e.g., github-actions.instructions.md) are auto-attached + // Track as copilotInstructions since they're part of the instructions system + refs.copilotInstructions++; } else { // For other files, increment the general file counter // This makes actual file attachments show up in context ref counts @@ -1960,6 +1969,17 @@ class CopilotTokenTracker implements vscode.Disposable { const pathKey = fsPath.length > 100 ? '...' + fsPath.substring(fsPath.length - 97) : fsPath; refs.byPath[pathKey] = (refs.byPath[pathKey] || 0) + 1; } + + // Handle symbol references (e.g., #sym:functionName) + // Symbol references have a 'name' field instead of fsPath + const symbolName = reference.name; + if (typeof symbolName === 'string' && kind === 'reference') { + // This is a symbol reference, track it + refs.symbol++; + // Track symbol by name for display (use 'name' as path) + const symbolKey = `#sym:${symbolName}`; + refs.byPath[symbolKey] = (refs.byPath[symbolKey] || 0) + 1; + } } } } @@ -1984,6 +2004,15 @@ class CopilotTokenTracker implements vscode.Disposable { refs.byKind[kind] = (refs.byKind[kind] || 0) + 1; } + // Handle symbol references (e.g., #sym:functionName) + // These appear as kind="generic" with name starting with "sym:" + if (kind === 'generic' && typeof variable.name === 'string' && variable.name.startsWith('sym:')) { + refs.symbol++; + // Track symbol by name for display + const symbolKey = `#${variable.name}`; + refs.byPath[symbolKey] = (refs.byPath[symbolKey] || 0) + 1; + } + // Process promptFile variables that contain file references if (kind === 'promptFile' && variable.value) { const value = variable.value; @@ -2483,10 +2512,86 @@ class CopilotTokenTracker implements vscode.Disposable { // Handle .jsonl files OR .json files with JSONL content (Copilot CLI format and VS Code incremental format) const isJsonlContent = sessionFile.endsWith('.jsonl') || this.isJsonlContent(fileContent); if (isJsonlContent) { - const lines = fileContent.trim().split('\n'); + const lines = fileContent.trim().split('\n').filter(l => l.trim()); const timestamps: number[] = []; const allContentReferences: any[] = []; // Collect for repository extraction + // Detect if this is delta-based format (VS Code incremental) + let isDeltaBased = false; + if (lines.length > 0) { + try { + const firstLine = JSON.parse(lines[0]); + if (firstLine && typeof firstLine.kind === 'number') { + isDeltaBased = true; + } + } catch { + // Not delta format + } + } + + if (isDeltaBased) { + // Delta-based format: reconstruct full state first, then extract details + let sessionState: any = {}; + for (const line of lines) { + try { + const delta = JSON.parse(line); + sessionState = this.applyDelta(sessionState, delta); + } catch { + // Skip invalid lines + } + } + + // Extract session metadata from reconstructed state + if (sessionState.creationDate) { + timestamps.push(sessionState.creationDate); + } + if (sessionState.customTitle) { + details.title = sessionState.customTitle; + } + + // Process reconstructed requests array + const requests = sessionState.requests || []; + details.interactions = requests.length; + + for (const request of requests) { + if (!request) { continue; } + + if (request.timestamp) { + timestamps.push(request.timestamp); + } + + // Analyze all context references from this request (unified method) + this.analyzeRequestContext(request, details.contextReferences); + + // Collect contentReferences for repository extraction + if (request.contentReferences && Array.isArray(request.contentReferences)) { + allContentReferences.push(...request.contentReferences); + } + } + + if (timestamps.length > 0) { + timestamps.sort((a, b) => a - b); + details.firstInteraction = new Date(timestamps[0]).toISOString(); + const lastTimestamp = new Date(timestamps[timestamps.length - 1]); + details.lastInteraction = lastTimestamp > stat.mtime + ? lastTimestamp.toISOString() + : stat.mtime.toISOString(); + } else { + details.lastInteraction = stat.mtime.toISOString(); + } + + // Extract repository from collected contentReferences + if (allContentReferences.length > 0) { + details.repository = await this.extractRepositoryFromContentReferences(allContentReferences); + } + + // Update cache with the details we just collected + await this.updateCacheWithSessionDetails(sessionFile, stat, details); + + return details; + } + + // Non-delta JSONL (Copilot CLI format) - process line-by-line for (const line of lines) { if (!line.trim()) { continue; } try { @@ -2503,46 +2608,6 @@ class CopilotTokenTracker implements vscode.Disposable { this.analyzeContextReferences(event.data.content, details.contextReferences); } } - - // Handle VS Code incremental .jsonl format (kind: 0, 1, 2) - // kind: 0 = session header with creationDate - // kind: 2 = requests array with timestamps - if (event.kind === 0 && event.v) { - // Session creation timestamp - if (event.v.creationDate) { - timestamps.push(event.v.creationDate); - } - // Session title - always update to get LAST title (matches VS Code UI) - if (event.v.customTitle) { - details.title = event.v.customTitle; - } - } - - if (event.kind === 2 && event.k?.[0] === 'requests' && Array.isArray(event.v)) { - // New requests being added - count interactions and extract timestamps - for (const request of event.v) { - if (request.requestId) { - details.interactions++; - } - if (request.timestamp) { - timestamps.push(request.timestamp); - } - // Analyze context references in request message - if (request.message?.text) { - this.analyzeContextReferences(request.message.text, details.contextReferences); - } - // Collect contentReferences for repository extraction - if (request.contentReferences && Array.isArray(request.contentReferences)) { - allContentReferences.push(...request.contentReferences); - } - - } - } - - // Check kind: 1 (value updates) for title changes - if (event.kind === 1 && event.k?.includes('customTitle') && event.v) { - details.title = event.v; - } } catch { // Skip malformed lines } @@ -2595,6 +2660,8 @@ class CopilotTokenTracker implements vscode.Disposable { timestamps.push(new Date(ts).getTime()); } + // Analyze all context references from this request + this.analyzeRequestContext(request, details.contextReferences); // Analyze context references if (request.message?.text) { this.analyzeContextReferences(request.message.text, details.contextReferences); @@ -2725,17 +2792,9 @@ class CopilotTokenTracker implements vscode.Disposable { const contextRefs = this.createEmptyContextRefs(); const userMessage = request.message?.text || ''; - this.analyzeContextReferences(userMessage, contextRefs); - // Analyze contentReferences from request - if (request.contentReferences && Array.isArray(request.contentReferences)) { - this.analyzeContentReferences(request.contentReferences, contextRefs); - } - - // Analyze variableData from request - if (request.variableData) { - this.analyzeVariableData(request.variableData, contextRefs); - } + // Analyze all context references from this request + this.analyzeRequestContext(request, contextRefs); // Get model from request or fall back to session model const requestModel = request.modelId || @@ -2859,13 +2918,7 @@ class CopilotTokenTracker implements vscode.Disposable { // Analyze context references const contextRefs = this.createEmptyContextRefs(); - this.analyzeContextReferences(userMessage, contextRefs); - if (request.variableData) { - const varDataStr = JSON.stringify(request.variableData).toLowerCase(); - if (varDataStr.includes('workspace')) { contextRefs.workspace++; } - if (varDataStr.includes('terminal')) { contextRefs.terminal++; } - if (varDataStr.includes('vscode')) { contextRefs.vscode++; } - } + this.analyzeRequestContext(request, contextRefs); // Extract model const model = this.getModelFromRequest(request); @@ -4337,7 +4390,6 @@ class CopilotTokenTracker implements vscode.Disposable { // Handle messages from the webview this.diagnosticsPanel.webview.onDidReceiveMessage(async (message) => { - this.log(`DEBUG Diagnostics webview message: ${JSON.stringify(message)}`); switch (message.command) { case 'copyReport': await vscode.env.clipboard.writeText(this.lastDiagnosticReport); diff --git a/src/types/css.d.ts b/src/types/css.d.ts new file mode 100644 index 0000000..31f07ea --- /dev/null +++ b/src/types/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: string; + export default content; +} diff --git a/src/webview/chart/main.ts b/src/webview/chart/main.ts index 6df9e07..bc6a4b4 100644 --- a/src/webview/chart/main.ts +++ b/src/webview/chart/main.ts @@ -1,6 +1,8 @@ // @ts-nocheck // Chart.js ESM bundle is loaded dynamically; skip CJS resolution noise import { el, createButton } from '../shared/domUtils'; import { BUTTONS } from '../shared/buttonConfig'; +// CSS imported as text via esbuild +import styles from './styles.css'; type ChartModule = typeof import('chart.js/auto'); type ChartConstructor = ChartModule['default']; @@ -64,32 +66,7 @@ function renderLayout(data: InitialChartData): void { root.replaceChildren(); const style = document.createElement('style'); - style.textContent = ` - :root { color: #e7e7e7; background: #0e0e0f; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } - body { margin: 0; background: #0e0e0f; } - .container { padding: 16px; display: flex; flex-direction: column; gap: 14px; max-width: 1200px; margin: 0 auto; } - .header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 4px; } - .header-left { display: flex; align-items: center; gap: 8px; } - .header-icon { font-size: 20px; } - .header-title { font-size: 16px; font-weight: 700; color: #fff; text-align: left; } - .button-row { display: flex; flex-wrap: wrap; gap: 8px; } - .section { background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); border: 1px solid #2e2e34; border-radius: 10px; padding: 12px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.28); text-align: center; } - .section h3 { margin: 0 0 10px 0; font-size: 14px; display: flex; align-items: center; gap: 6px; color: #ffffff; letter-spacing: 0.2px; text-align: left; } - .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; text-align: center; } - .card { background: #1b1b1e; border: 1px solid #2a2a30; border-radius: 8px; padding: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.24); text-align: center; } - .card-label { color: #b8b8b8; font-size: 11px; margin-bottom: 6px; } - .card-value { color: #f6f6f6; font-size: 18px; font-weight: 700; } - .card-sub { color: #9aa0a6; font-size: 11px; margin-top: 2px; } - .chart-shell { background: #1b1b1e; border: 1px solid #2a2a30; border-radius: 10px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.22); text-align: center; } - .chart-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; justify-content: center; } - .toggle { background: #202024; border: 1px solid #2d2d33; color: #e7e7e7; padding: 8px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: all 0.15s ease; } - .toggle.active { background: #0e639c; border-color: #1177bb; color: #fff; } - .toggle:hover { background: #2a2a30; } - .toggle.active:hover { background: #1177bb; } - .canvas-wrap { position: relative; height: 420px; } - .footer { color: #a0a0a0; font-size: 11px; margin-top: 6px; text-align: center; } - .footer em { color: #c0c0c0; } - `; + style.textContent = styles; const container = el('div', 'container'); const header = el('div', 'header'); diff --git a/src/webview/chart/styles.css b/src/webview/chart/styles.css new file mode 100644 index 0000000..6d2d759 --- /dev/null +++ b/src/webview/chart/styles.css @@ -0,0 +1,162 @@ +:root { + color: #e7e7e7; + background: #0e0e0f; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +body { + margin: 0; + background: #0e0e0f; +} + +.container { + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding-bottom: 4px; +} + +.header-left { + display: flex; + align-items: center; + gap: 8px; +} + +.header-icon { + font-size: 20px; +} + +.header-title { + font-size: 16px; + font-weight: 700; + color: #fff; + text-align: left; +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.section { + background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); + border: 1px solid #2e2e34; + border-radius: 10px; + padding: 12px; + box-shadow: 0 4px 10px rgb(0, 0, 0, 0.28); + text-align: center; +} + +.section h3 { + margin: 0 0 10px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + color: #fff; + letter-spacing: 0.2px; + text-align: left; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + text-align: center; +} + +.card { + background: #1b1b1e; + border: 1px solid #2a2a30; + border-radius: 8px; + padding: 12px; + box-shadow: 0 2px 6px rgb(0, 0, 0, 0.24); + text-align: center; +} + +.card-label { + color: #b8b8b8; + font-size: 11px; + margin-bottom: 6px; +} + +.card-value { + color: #f6f6f6; + font-size: 18px; + font-weight: 700; +} + +.card-sub { + color: #9aa0a6; + font-size: 11px; + margin-top: 2px; +} + +.chart-shell { + background: #1b1b1e; + border: 1px solid #2a2a30; + border-radius: 10px; + padding: 12px; + box-shadow: 0 2px 8px rgb(0, 0, 0, 0.22); + text-align: center; +} + +.chart-controls { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; + justify-content: center; +} + +.toggle { + background: #202024; + border: 1px solid #2d2d33; + color: #e7e7e7; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; +} + +.toggle.active { + background: #0e639c; + border-color: #17b; + color: #fff; +} + +.toggle:hover { + background: #2a2a30; +} + +.toggle.active:hover { + background: #17b; +} + +.canvas-wrap { + position: relative; + height: 420px; +} + +.footer { + color: #a0a0a0; + font-size: 11px; + margin-top: 6px; + text-align: center; +} + +.footer em { + color: #c0c0c0; +} diff --git a/src/webview/details/main.ts b/src/webview/details/main.ts index 19252db..bd6d94a 100644 --- a/src/webview/details/main.ts +++ b/src/webview/details/main.ts @@ -6,6 +6,8 @@ import { BUTTONS } from '../shared/buttonConfig'; // Token estimators loaded from JSON // @ts-ignore import tokenEstimatorsJson from '../../tokenEstimators.json'; +// CSS imported as text via esbuild +import styles from './styles.css'; type ModelUsage = Record; type EditorUsage = Record; @@ -100,36 +102,7 @@ function renderShell( root.replaceChildren(); const style = document.createElement('style'); - style.textContent = ` - :root { - color: #e7e7e7; - background: #1e1e1e; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - } - body { margin: 0; background: #0e0e0f; } - .container { padding: 16px; display: flex; flex-direction: column; gap: 14px; max-width: 1200px; margin: 0 auto; } - .header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 4px; } - .title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; color: #fff; } - .button-row { display: flex; flex-wrap: wrap; gap: 8px; } - .sections { display: flex; flex-direction: column; gap: 16px; } - .section { background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); border: 1px solid #2e2e34; border-radius: 10px; padding: 12px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.28); } - .section h3 { margin: 0 0 10px 0; font-size: 14px; display: flex; align-items: center; gap: 6px; color: #ffffff; letter-spacing: 0.2px; } - .stats-table { width: 100%; border-collapse: collapse; table-layout: fixed; background: #1b1b1e; border: 1px solid #2a2a30; border-radius: 8px; overflow: hidden; } - .stats-table thead { background: #242429; } - .stats-table th, .stats-table td { padding: 10px 12px; border-bottom: 1px solid #2d2d33; vertical-align: middle; } - .stats-table th { text-align: left; color: #d0d0d0; font-weight: 700; font-size: 12px; letter-spacing: 0.1px; } - .stats-table td { color: #f0f0f0; font-size: 12px; } - .stats-table th.align-right, .stats-table td.align-right { text-align: right; } - .stats-table tbody tr:nth-child(even) { background: #18181b; } - .metric-label { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; } - .period-header { display: flex; align-items: center; gap: 4px; color: #c8c8c8; } - .align-right .period-header { justify-content: flex-end; } - .value-right { text-align: right; } - .muted { color: #a0a0a0; font-size: 11px; margin-top: 4px; } - .notes { margin: 4px 0 0 0; padding-left: 16px; color: #c8c8c8; } - .notes li { margin: 4px 0; line-height: 1.4; } - .footer { color: #a0a0a0; font-size: 11px; margin-top: 6px; } - `; + style.textContent = styles; const container = el('div', 'container'); const header = el('div', 'header'); diff --git a/src/webview/details/styles.css b/src/webview/details/styles.css new file mode 100644 index 0000000..4fffe5d --- /dev/null +++ b/src/webview/details/styles.css @@ -0,0 +1,154 @@ +:root { + color: #e7e7e7; + background: #1e1e1e; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +body { + margin: 0; + background: #0e0e0f; +} + +.container { + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding-bottom: 4px; +} + +.title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 700; + color: #fff; +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sections { + display: flex; + flex-direction: column; + gap: 16px; +} + +.section { + background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); + border: 1px solid #2e2e34; + border-radius: 10px; + padding: 12px; + box-shadow: 0 4px 10px rgb(0, 0, 0, 0.28); +} + +.section h3 { + margin: 0 0 10px; + font-size: 14px; + display: flex; + align-items: center; + gap: 6px; + color: #fff; + letter-spacing: 0.2px; +} + +.stats-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + background: #1b1b1e; + border: 1px solid #2a2a30; + border-radius: 8px; + overflow: hidden; +} + +.stats-table thead { + background: #242429; +} + +.stats-table th, +.stats-table td { + padding: 10px 12px; + border-bottom: 1px solid #2d2d33; + vertical-align: middle; +} + +.stats-table th { + text-align: left; + color: #d0d0d0; + font-weight: 700; + font-size: 12px; + letter-spacing: 0.1px; +} + +.stats-table td { + color: #f0f0f0; + font-size: 12px; +} + +.stats-table th.align-right, +.stats-table td.align-right { + text-align: right; +} + +.stats-table tbody tr:nth-child(even) { + background: #18181b; +} + +.metric-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-weight: 600; +} + +.period-header { + display: flex; + align-items: center; + gap: 4px; + color: #c8c8c8; +} + +.align-right .period-header { + justify-content: flex-end; +} + +.value-right { + text-align: right; +} + +.muted { + color: #a0a0a0; + font-size: 11px; + margin-top: 4px; +} + +.notes { + margin: 4px 0 0; + padding-left: 16px; + color: #c8c8c8; +} + +.notes li { + margin: 4px 0; + line-height: 1.4; +} + +.footer { + color: #a0a0a0; + font-size: 11px; + margin-top: 6px; +} diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index 991053d..5de96d1 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -1,5 +1,7 @@ // Diagnostics Report webview with tabbed interface import { buttonHtml } from '../shared/buttonConfig'; +// CSS imported as text via esbuild +import styles from './styles.css'; // Constants const LOADING_PLACEHOLDER = 'Loading...'; @@ -9,20 +11,7 @@ const LOADING_MESSAGE = `⏳ Loading diagnostic data... This may take a few moments depending on the number of session files. The view will automatically update when data is ready.`; -type ContextReferenceUsage = { - file: number; - selection: number; - implicitSelection: number; - symbol: number; - codebase: number; - workspace: number; - terminal: number; - vscode: number; - byKind: { [kind: string]: number }; - copilotInstructions: number; - agentsMd: number; - byPath: { [path: string]: number }; -}; +import { ContextReferenceUsage, getTotalContextRefs, getContextRefsSummary } from '../shared/contextRefUtils'; type SessionFileDetails = { file: string; @@ -92,6 +81,7 @@ const initialData = window.__INITIAL_DIAGNOSTICS__; let currentSortColumn: 'lastInteraction' = 'lastInteraction'; let currentSortDirection: 'asc' | 'desc' = 'desc'; let currentEditorFilter: string | null = null; // null = show all +let currentContextRefFilter: keyof ContextReferenceUsage | null = null; // null = show all function escapeHtml(text: string): string { return text @@ -150,26 +140,6 @@ function sanitizeNumber(value: number | undefined | null): string { return value.toString(); } -function getTotalContextRefs(refs: ContextReferenceUsage): number { - return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + - refs.workspace + refs.terminal + refs.vscode + refs.copilotInstructions + refs.agentsMd; -} - -function getContextRefsSummary(refs: ContextReferenceUsage): string { - const parts: string[] = []; - if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } - if (refs.selection > 0) { parts.push(`#sel: ${refs.selection}`); } - if (refs.implicitSelection > 0) { parts.push(`impl: ${refs.implicitSelection}`); } - if (refs.symbol > 0) { parts.push(`#sym: ${refs.symbol}`); } - if (refs.codebase > 0) { parts.push(`#cb: ${refs.codebase}`); } - if (refs.workspace > 0) { parts.push(`@ws: ${refs.workspace}`); } - if (refs.terminal > 0) { parts.push(`@term: ${refs.terminal}`); } - if (refs.vscode > 0) { parts.push(`@vsc: ${refs.vscode}`); } - if (refs.copilotInstructions > 0) { parts.push(`📋 inst: ${refs.copilotInstructions}`); } - if (refs.agentsMd > 0) { parts.push(`🤖 ag: ${refs.agentsMd}`); } - return parts.length > 0 ? parts.join(', ') : 'None'; -} - function getFileName(filePath: string): string { const parts = filePath.split(/[/\\]/); return parts[parts.length - 1]; @@ -293,14 +263,39 @@ function renderSessionTable(detailedFiles: SessionFileDetails[], isLoading: bool const editors = Object.keys(editorStats).sort(); // Apply editor filter - const filteredFiles = currentEditorFilter + let filteredFiles = currentEditorFilter ? detailedFiles.filter(sf => sf.editorSource === currentEditorFilter) : detailedFiles; + // Apply context ref filter + if (currentContextRefFilter) { + filteredFiles = filteredFiles.filter(sf => { + const refType = currentContextRefFilter!; // Assert non-null since we're inside the if block + const value = sf.contextReferences[refType]; + return typeof value === 'number' && value > 0; + }); + } + // Summary stats for filtered files const totalInteractions = filteredFiles.reduce((sum, sf) => sum + Number(sf.interactions || 0), 0); const totalContextRefs = filteredFiles.reduce((sum, sf) => sum + getTotalContextRefs(sf.contextReferences), 0); + // Aggregate context ref breakdown + const aggContextRefs = filteredFiles.reduce((agg, sf) => { + const r = sf.contextReferences; + agg.file += r.file; + agg.symbol += r.symbol; + agg.selection += r.selection; + agg.implicitSelection += r.implicitSelection; + agg.codebase += r.codebase; + agg.workspace += r.workspace; + agg.terminal += r.terminal; + agg.vscode += r.vscode; + agg.copilotInstructions += r.copilotInstructions; + agg.agentsMd += r.agentsMd; + return agg; + }, { file: 0, symbol: 0, selection: 0, implicitSelection: 0, codebase: 0, workspace: 0, terminal: 0, vscode: 0, copilotInstructions: 0, agentsMd: 0 }); + // Sort filtered files const sortedFiles = sortSessionFiles(filteredFiles); @@ -337,6 +332,16 @@ function renderSessionTable(detailedFiles: SessionFileDetails[], isLoading: bool
🔗 Context References
${safeText(totalContextRefs)}
+
+ ${totalContextRefs === 0 ? 'None' : ''} + ${aggContextRefs.file > 0 ? `
#file ${aggContextRefs.file}
` : ''} + ${aggContextRefs.symbol > 0 ? `
#sym ${aggContextRefs.symbol}
` : ''} + ${aggContextRefs.implicitSelection > 0 ? `
implicit ${aggContextRefs.implicitSelection}
` : ''} + ${aggContextRefs.copilotInstructions > 0 ? `
📋 instructions ${aggContextRefs.copilotInstructions}
` : ''} + ${aggContextRefs.agentsMd > 0 ? `
🤖 agents ${aggContextRefs.agentsMd}
` : ''} + ${aggContextRefs.workspace > 0 ? `
@workspace ${aggContextRefs.workspace}
` : ''} + ${aggContextRefs.vscode > 0 ? `
@vscode ${aggContextRefs.vscode}
` : ''} +
📅 Time Range
@@ -604,258 +609,7 @@ function renderLayout(data: DiagnosticsData): void { const detailedFiles = data.detailedSessionFiles || []; root.innerHTML = ` - +
@@ -1154,8 +908,6 @@ function renderLayout(data: DiagnosticsData): void { btnTab.disabled = false; } - console.log('DEBUG Cache cleared confirmation received'); - // Re-enable buttons after a short delay and reset to original state setTimeout(() => { if (btnReport) { @@ -1191,7 +943,6 @@ function renderLayout(data: DiagnosticsData): void { if (ageValue) { ageValue.textContent = '0 seconds ago'; } } } - console.log('DEBUG Cache refreshed with new data:', cacheInfo); } } }); @@ -1272,6 +1023,25 @@ function setupStorageLinkHandlers(): void { }); }); } + + // Wire up context ref filter handlers + function setupContextRefFilterHandlers(): void { + document.querySelectorAll('.context-ref-filter').forEach(filter => { + filter.addEventListener('click', () => { + const refType = (filter as HTMLElement).getAttribute('data-ref-type') as keyof ContextReferenceUsage | null; + + // Toggle: if clicking the same filter, clear it + if (currentContextRefFilter === refType) { + currentContextRefFilter = null; + } else { + currentContextRefFilter = refType; + } + + // Re-render table + reRenderTable(); + }); + }); + } // Re-render the session table with current filter/sort state function reRenderTable(): void { @@ -1281,6 +1051,7 @@ function setupStorageLinkHandlers(): void { if (!isLoading) { setupSortHandlers(); setupEditorFilterHandlers(); + setupContextRefFilterHandlers(); setupFileLinks(); } } @@ -1346,7 +1117,6 @@ function setupStorageLinkHandlers(): void { } document.getElementById('btn-clear-cache')?.addEventListener('click', () => { - console.log('DEBUG Clear cache button clicked (report tab)'); const btn = document.getElementById('btn-clear-cache') as HTMLButtonElement | null; if (btn) { btn.style.background = '#d97706'; @@ -1359,7 +1129,6 @@ function setupStorageLinkHandlers(): void { }); document.getElementById('btn-clear-cache-tab')?.addEventListener('click', () => { - console.log('DEBUG Clear cache button clicked (cache tab)'); const btn = document.getElementById('btn-clear-cache-tab') as HTMLButtonElement | null; if (btn) { btn.style.background = '#d97706'; @@ -1376,7 +1145,6 @@ function setupStorageLinkHandlers(): void { const target = event.target as HTMLElement; if (!target) { return; } if (target.id === 'btn-clear-cache' || target.id === 'btn-clear-cache-tab') { - console.log('DEBUG Clear cache button clicked via delegated handler', target.id); target.style.background = '#d97706'; target.innerHTML = 'Clearing...'; if (target instanceof HTMLButtonElement) { @@ -1395,17 +1163,16 @@ function setupStorageLinkHandlers(): void { // Backend configuration buttons document.getElementById('btn-configure-backend')?.addEventListener('click', () => { - console.log('[DEBUG] Configure backend button clicked'); vscode.postMessage({ command: 'configureBackend' }); }); document.getElementById('btn-open-settings')?.addEventListener('click', () => { - console.log('[DEBUG] Open settings button clicked'); vscode.postMessage({ command: 'openSettings' }); }); setupSortHandlers(); setupEditorFilterHandlers(); + setupContextRefFilterHandlers(); setupFileLinks(); setupStorageLinkHandlers(); diff --git a/src/webview/diagnostics/styles.css b/src/webview/diagnostics/styles.css new file mode 100644 index 0000000..ada4cb6 --- /dev/null +++ b/src/webview/diagnostics/styles.css @@ -0,0 +1,423 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0e0e0f; + color: #e7e7e7; + padding: 16px; + line-height: 1.5; + min-width: 320px; +} + +.container { + background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); + border: 1px solid #2e2e34; + border-radius: 10px; + padding: 16px; + box-shadow: 0 4px 10px rgb(0, 0, 0, 0.28); + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 4px; +} + +.header-left { + display: flex; + align-items: center; + gap: 8px; +} + +.header-icon { + font-size: 20px; +} + +.header-title { + font-size: 16px; + font-weight: 700; + color: #fff; +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* Tab styles */ +.tabs { + display: flex; + border-bottom: 1px solid #5a5a5a; + margin-bottom: 16px; +} + +.tab { + padding: 10px 20px; + cursor: pointer; + border: none; + background: transparent; + color: #999; + font-size: 13px; + font-weight: 500; + border-bottom: 2px solid transparent; + transition: all 0.2s; +} + +.tab:hover { + color: #fff; + background: rgb(255, 255, 255, 0.05); +} + +.tab.active { + color: #4FC3F7; + border-bottom-color: #4FC3F7; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Editor filter panels */ +.editor-filter-panels { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 16px; +} + +.editor-panel { + background: #353535; + border: 2px solid #5a5a5a; + border-radius: 8px; + padding: 12px 16px; + cursor: pointer; + transition: all 0.2s; + min-width: 140px; + text-align: center; +} + +.editor-panel:hover { + background: #404040; + border-color: #7a7a7a; +} + +.editor-panel.active { + background: #3a4a5a; + border-color: #4FC3F7; +} + +.editor-panel-icon { + font-size: 24px; + margin-bottom: 4px; +} + +.editor-panel-name { + font-size: 13px; + font-weight: 600; + color: #fff; + margin-bottom: 2px; +} + +.editor-panel-stats { + font-size: 10px; + color: #999; +} + +/* Loading state */ +.loading-state { + text-align: center; + padding: 40px 20px; + color: #999; +} + +.loading-spinner { + font-size: 48px; + margin-bottom: 16px; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +.loading-text { + font-size: 16px; + color: #fff; + margin-bottom: 8px; +} + +.loading-subtext { + font-size: 12px; + color: #888; +} + +/* Summary cards */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.summary-card { + background: #353535; + border: 1px solid #5a5a5a; + border-radius: 4px; + padding: 12px; + text-align: center; +} + +.summary-label { + font-size: 11px; + color: #b3b3b3; + margin-bottom: 4px; +} + +.summary-value { + font-size: 18px; + font-weight: 600; + color: #fff; +} + +.summary-sub { + font-size: 10px; + color: #94a3b8; + text-align: left; + margin-top: 6px; +} + +.context-ref-filter { + cursor: pointer; + padding: 2px 6px; + border-radius: 3px; + margin: 2px 0; + transition: all 0.2s; +} + +.context-ref-filter:hover { + background: rgb(79, 195, 247, 0.2); + color: #4FC3F7; +} + +.context-ref-filter.active { + background: rgb(79, 195, 247, 0.3); + color: #4FC3F7; + font-weight: 600; +} + +/* Table styles */ +.table-container { + overflow: auto; + max-height: 500px; +} + +.session-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.session-table th, +.session-table td { + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid #5a5a5a; +} + +.session-table th { + background: #353535; + color: #fff; + font-weight: 600; + position: sticky; + top: 0; +} + +.session-table th.sortable { + cursor: pointer; + user-select: none; +} + +.session-table th.sortable:hover { + background: #454545; + color: #4FC3F7; +} + +.session-table tr:hover { + background: rgb(255, 255, 255, 0.03); +} + +.editor-badge { + background: #4a5a6a; + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + color: #fff; +} + +.session-folders-table { + margin-top: 16px; + margin-bottom: 16px; +} + +.session-folders-table h4 { + color: #fff; + font-size: 14px; + margin-bottom: 12px; +} + +.report-content { + background: #2a2a2a; + border: 1px solid #5a5a5a; + border-radius: 4px; + padding: 16px; + white-space: pre-wrap; + font-size: 13px; + overflow: auto; + max-height: 60vh; +} + +.file-subpath { + font-size: 11px; + color: #9aa0a6; + margin-top: 4px; +} + +.session-file-link, +.reveal-link, +.view-formatted-link { + color: #4FC3F7; + text-decoration: underline; + cursor: pointer; +} + +.session-file-link:hover, +.reveal-link:hover, +.view-formatted-link:hover { + color: #81D4FA; +} + +.empty-session-link { + color: #999; +} + +.empty-session-link:hover { + color: #aaa; +} + +.button-group { + display: flex; + gap: 12px; + margin-top: 16px; + flex-wrap: wrap; +} + +.button { + background: #202024; + border: 1px solid #2d2d33; + color: #e7e7e7; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: background-color 0.15s ease; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.button:hover { + background: #2a2a30; +} + +.button:active { + background: #0a5a8a; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.button.secondary { + background: #3c3c3c; + border-color: #5a5a5a; + color: #fff; +} + +.button.secondary:hover { + background: #4a4a4a; +} + +.info-box { + background: #3a4a5a; + border: 1px solid #4a5a6a; + border-radius: 4px; + padding: 12px; + margin-bottom: 16px; + font-size: 13px; +} + +.info-box-title { + font-weight: 600; + color: #fff; + margin-bottom: 6px; +} + +.cache-details { + margin-top: 16px; +} + +.cache-location { + margin-top: 20px; +} + +.cache-location h4 { + color: #fff; + font-size: 14px; + margin-bottom: 8px; +} + +.location-box { + background: #2a2a2a; + border: 1px solid #5a5a5a; + border-radius: 4px; + padding: 12px; + overflow-x: auto; +} + +.location-box code { + color: #4FC3F7; + font-size: 12px; +} + +.cache-actions { + margin-top: 20px; +} + +.cache-actions h4 { + color: #fff; + font-size: 14px; + margin-bottom: 8px; +} diff --git a/src/webview/logviewer/main.ts b/src/webview/logviewer/main.ts index f082897..6d5e3d1 100644 --- a/src/webview/logviewer/main.ts +++ b/src/webview/logviewer/main.ts @@ -1,18 +1,7 @@ // Log Viewer webview - displays session file details and chat turns -type ContextReferenceUsage = { - file: number; - selection: number; - implicitSelection: number; - symbol: number; - codebase: number; - workspace: number; - terminal: number; - vscode: number; - byKind: { [kind: string]: number }; - copilotInstructions: number; - agentsMd: number; - byPath: { [path: string]: number }; -}; +import { ContextReferenceUsage, getTotalContextRefs, getImplicitContextRefs, getExplicitContextRefs, getContextRefsSummary } from '../shared/contextRefUtils'; +// CSS imported as text via esbuild +import styles from './styles.css'; type ChatTurn = { turnNumber: number; @@ -101,26 +90,6 @@ function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } -function getTotalContextRefs(refs: ContextReferenceUsage): number { - return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + - refs.workspace + refs.terminal + refs.vscode + refs.copilotInstructions + refs.agentsMd; -} - -function getContextRefsSummary(refs: ContextReferenceUsage): string { - const parts: string[] = []; - if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } - if (refs.selection > 0) { parts.push(`#selection: ${refs.selection}`); } - if (refs.implicitSelection > 0) { parts.push(`implicit: ${refs.implicitSelection}`); } - if (refs.symbol > 0) { parts.push(`#symbol: ${refs.symbol}`); } - if (refs.codebase > 0) { parts.push(`#codebase: ${refs.codebase}`); } - if (refs.workspace > 0) { parts.push(`@workspace: ${refs.workspace}`); } - if (refs.terminal > 0) { parts.push(`@terminal: ${refs.terminal}`); } - if (refs.vscode > 0) { parts.push(`@vscode: ${refs.vscode}`); } - if (refs.copilotInstructions > 0) { parts.push(`📋 instructions: ${refs.copilotInstructions}`); } - if (refs.agentsMd > 0) { parts.push(`🤖 agents: ${refs.agentsMd}`); } - return parts.length > 0 ? parts.join(', ') : 'None'; -} - function getContextRefBadges(refs: ContextReferenceUsage): string { const badges: string[] = []; if (refs.selection > 0) { badges.push(`#selection: ${refs.selection}`); } @@ -135,25 +104,95 @@ function getContextRefBadges(refs: ContextReferenceUsage): string { } function renderContextReferencesDetailed(refs: ContextReferenceUsage): string { - const sections: string[] = []; + const rows: { category: string; name: string; count: number; type: 'implicit' | 'explicit' }[] = []; - // Show instruction file references - if (refs.copilotInstructions > 0 || refs.agentsMd > 0) { - const instrRefs: string[] = []; - if (refs.copilotInstructions > 0) { instrRefs.push(`📋 copilot-instructions: ${refs.copilotInstructions}`); } - if (refs.agentsMd > 0) { instrRefs.push(`🤖 agents.md: ${refs.agentsMd}`); } - sections.push(`
Instructions: ${instrRefs.join(', ')}
`); + // Implicit selections (implicit) + if (refs.implicitSelection > 0) { + rows.push({ category: '📝 Selection', name: 'editor selection', count: refs.implicitSelection, type: 'implicit' }); } - // Show file paths if any + // File paths and symbols from byPath if (refs.byPath && Object.keys(refs.byPath).length > 0) { - const pathList = Object.entries(refs.byPath) - .map(([path, count]) => `${getFileName(path)}: ${count}`) - .join(', '); - sections.push(`
Files: ${pathList}
`); + Object.entries(refs.byPath).forEach(([path, count]) => { + if (path.startsWith('#sym:')) { + // Symbols are explicit user references + rows.push({ category: '🔣 Symbol', name: path.substring(5), count, type: 'explicit' }); + } else { + // Check if this is an instruction file (implicit) or regular file (explicit) + const normalizedPath = path.replace(/\\/g, '/').toLowerCase(); + const isInstructionFile = normalizedPath.includes('copilot-instructions.md') || + normalizedPath.endsWith('.instructions.md') || + normalizedPath.endsWith('/agents.md'); + if (isInstructionFile) { + rows.push({ category: '📋 Instructions', name: getFileName(path), count, type: 'implicit' }); + } else { + // Regular file references are explicit + rows.push({ category: '📁 File', name: getFileName(path), count, type: 'explicit' }); + } + } + }); + } + + // Instruction counters that aren't in byPath (fallback) + // Only show if we haven't already added instruction files from byPath + const hasInstructionFiles = rows.some(r => r.category === '📋 Instructions'); + if (!hasInstructionFiles) { + if (refs.copilotInstructions > 0) { + rows.push({ category: '📋 Instructions', name: 'copilot-instructions', count: refs.copilotInstructions, type: 'implicit' }); + } + if (refs.agentsMd > 0) { + rows.push({ category: '🤖 Agents', name: 'agents.md', count: refs.agentsMd, type: 'implicit' }); + } + } + + // Explicit @ references + if (refs.workspace > 0) { + rows.push({ category: '🌐 Workspace', name: '@workspace', count: refs.workspace, type: 'explicit' }); + } + if (refs.terminal > 0) { + rows.push({ category: '💻 Terminal', name: '@terminal', count: refs.terminal, type: 'explicit' }); + } + if (refs.vscode > 0) { + rows.push({ category: '⚙️ VS Code', name: '@vscode', count: refs.vscode, type: 'explicit' }); + } + if (refs.codebase > 0) { + rows.push({ category: '📚 Codebase', name: '#codebase', count: refs.codebase, type: 'explicit' }); + } + if (refs.selection > 0) { + rows.push({ category: '✂️ Selection', name: '#selection', count: refs.selection, type: 'explicit' }); + } + + if (rows.length === 0) { + return '
No context references
'; } - return sections.length > 0 ? sections.join('') : '
No additional details
'; + // Build table + const tableRows = rows.map(row => { + const typeClass = row.type === 'implicit' ? 'context-type-implicit' : 'context-type-explicit'; + const typeLabel = row.type === 'implicit' ? '🔒 implicit' : '👤 explicit'; + return ` + ${row.category} + ${escapeHtml(row.name)} + ${row.count} + ${typeLabel} + `; + }).join(''); + + return ` + + + + + + + + + + + ${tableRows} + +
CategoryReferenceCountType
+ `; } function getTopEntries(map: { [key: string]: number } = {}, limit = 3): { key: string; value: number }[] { @@ -214,7 +253,13 @@ function renderTurnCard(turn: ChatTurn): string { }); otherPaths.forEach(([path]) => { - contextFileBadges.push(`📄 ${escapeHtml(getFileName(path))}`); + // Check if this is a symbol reference + if (path.startsWith('#sym:')) { + const symbolName = path.substring(5); // Remove '#sym:' prefix + contextFileBadges.push(`🔤 ${escapeHtml(symbolName)}`); + } else { + contextFileBadges.push(`📄 ${escapeHtml(getFileName(path))}`); + } }); } @@ -345,6 +390,8 @@ function renderLayout(data: SessionLogData): void { const usageTopMcpTools = usage ? getTopEntries(usage.mcpTools.byTool, 3) : []; const usageContextRefs = usage?.contextReferences || data.contextReferences; const usageContextTotal = getTotalContextRefs(usageContextRefs); + const usageContextImplicit = getImplicitContextRefs(usageContextRefs); + const usageContextExplicit = getExplicitContextRefs(usageContextRefs); const formatTopList = (entries: { key: string; value: number }[], mapper?: (k: string) => string) => { if (!entries.length) { return 'None'; } @@ -374,641 +421,7 @@ function renderLayout(data: SessionLogData): void { const modelNames = Object.keys(modelUsage); root.innerHTML = ` - +
@@ -1036,13 +449,7 @@ function renderLayout(data: SessionLogData): void {
🔗 Context Refs
${usageContextTotal}
- ${usageContextTotal === 0 ? 'None' : ''} - ${usageContextRefs.file > 0 ? `
#file ${usageContextRefs.file}
` : ''} - ${usageContextRefs.implicitSelection > 0 ? `
implicit ${usageContextRefs.implicitSelection}
` : ''} - ${usageContextRefs.copilotInstructions > 0 ? `
📋 instructions ${usageContextRefs.copilotInstructions}
` : ''} - ${usageContextRefs.agentsMd > 0 ? `
🤖 agents ${usageContextRefs.agentsMd}
` : ''} - ${usageContextRefs.workspace > 0 ? `
@workspace ${usageContextRefs.workspace}
` : ''} - ${usageContextRefs.vscode > 0 ? `
@vscode ${usageContextRefs.vscode}
` : ''} + ${usageContextTotal === 0 ? 'None' : `implicit ${usageContextImplicit}, explicit ${usageContextExplicit}`}
diff --git a/src/webview/logviewer/styles.css b/src/webview/logviewer/styles.css new file mode 100644 index 0000000..912903f --- /dev/null +++ b/src/webview/logviewer/styles.css @@ -0,0 +1,746 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0e0e0f; + color: #e7e7e7; + padding: 20px; + line-height: 1.6; + min-width: 320px; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +/* Mode/model bar improvements */ +.mode-bar-group { + background: linear-gradient(135deg, #1a1a22 0%, #1f1f28 100%); + border: 1px solid #3a3a44; + border-radius: 12px; + padding: 20px 24px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 28px; + margin-bottom: 24px; + box-shadow: 0 4px 12px rgb(0, 0, 0, 0.3), 0 1px 3px rgb(0, 0, 0, 0.2); +} + +.mode-bar { + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + font-weight: 500; +} + +.mode-icon { + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + background: #23232a; + border: 2px solid #2a2a30; + box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2); +} + +.mode-label { + color: #b8b8c0; + font-weight: 600; +} + +.mode-count { + color: #fff; + font-weight: 700; + font-size: 18px; +} + +.model-summary { + margin-left: auto; + font-size: 15px; + font-weight: 600; + color: #fff; + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.model-list { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.model-item { + background: linear-gradient(135deg, #2a2a35 0%, #25252f 100%); + border: 1px solid #3a3a44; + border-radius: 6px; + padding: 4px 12px; + color: #60a5fa; + font-weight: 600; + font-size: 13px; + box-shadow: 0 2px 6px rgb(0, 0, 0, 0.15); +} + +/* Summary cards */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.summary-card { + background: linear-gradient(135deg, #1a1a22 0%, #1f1f28 100%); + border: 1px solid #3a3a44; + border-radius: 12px; + padding: 24px 16px; + text-align: center; + box-shadow: 0 4px 12px rgb(0, 0, 0, 0.3), 0 1px 3px rgb(0, 0, 0, 0.2); + transition: transform 0.2s, box-shadow 0.2s; +} + +.summary-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgb(0, 0, 0, 0.4), 0 2px 4px rgb(0, 0, 0, 0.2); +} + +.filename-link { + cursor: pointer; + color: #60a5fa; + text-decoration: underline; + transition: color 0.2s; +} + +.filename-link:hover { + color: #93c5fd; +} + +.summary-label { + font-size: 14px; + color: #b8b8c0; + margin-bottom: 8px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.summary-value { + font-size: 32px; + font-weight: 700; + color: #60a5fa; + margin-bottom: 8px; +} + +.summary-sub { + font-size: 12px; + color: #94a3b8; + line-height: 1.5; +} + +/* Turns container */ +.turns-header { + font-size: 18px; + font-weight: 700; + color: #fff; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 0; + border-bottom: 2px solid #3a3a44; +} + +.turns-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +/* Turn card */ +.turn-card { + background: linear-gradient(135deg, #1a1a22 0%, #1f1f28 100%); + border: 1px solid #3a3a44; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgb(0, 0, 0, 0.3), 0 1px 3px rgb(0, 0, 0, 0.2); + transition: transform 0.2s, box-shadow 0.2s; +} + +.turn-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgb(0, 0, 0, 0.4), 0 2px 4px rgb(0, 0, 0, 0.2); +} + +.turn-header { + background: linear-gradient(135deg, #22222a 0%, #27272f 100%); + padding: 14px 16px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + border-bottom: 1px solid #3a3a44; + min-height: 48px; +} + +.turn-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: nowrap; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.turn-number { + font-weight: 700; + color: #fff; + font-size: 16px; + background: #3a3a44; + padding: 4px 10px; + border-radius: 6px; + flex-shrink: 0; +} + +.turn-mode { + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + font-weight: 700; + color: #fff; + box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2); + flex-shrink: 0; + white-space: nowrap; +} + +.turn-model { + font-size: 12px; + color: #94a3b8; + background: #2a2a35; + padding: 4px 10px; + border-radius: 6px; + font-weight: 600; + border: 1px solid #3a3a44; + flex-shrink: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.turn-tokens { + font-size: 12px; + color: #94a3b8; + font-weight: 600; + flex-shrink: 0; + white-space: nowrap; +} + +.context-badge { + font-size: 12px; + color: #e0e7ff; + background: linear-gradient(135deg, #4c1d95 0%, #5b21b6 100%); + padding: 4px 10px; + border-radius: 6px; + font-weight: 600; + border: 1px solid #6d28d9; + flex-shrink: 0; + white-space: nowrap; + margin-left: 4px; + box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2); +} + +/* Messages */ +.turn-content { + padding: 16px; +} + +.message { + margin-bottom: 14px; +} + +.message:last-child { + margin-bottom: 0; +} + +.message-label { + font-size: 12px; + font-weight: 700; + color: #94a3b8; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.message-text { + background: #22222a; + border-radius: 8px; + padding: 14px 16px; + font-size: 14px; + line-height: 1.6; + white-space: pre-wrap; + overflow-wrap: break-word; + max-height: 400px; + overflow-y: auto; + border: 1px solid #3a3a44; +} + +.user-message .message-text { + border-left: 4px solid #60a5fa; + background: linear-gradient(135deg, #1e293b 0%, #22222a 100%); +} + +.assistant-message .message-text { + border-left: 4px solid #7c3aed; + background: linear-gradient(135deg, #1e1e2a 0%, #22222a 100%); +} + +/* Shared collapse arrow for details/summary panels */ +.collapse-arrow { + display: inline-block; + width: 16px; + color: #94a3b8; + font-size: 10px; + transition: transform 0.2s; + flex-shrink: 0; +} + +details[open] > summary .collapse-arrow { + transform: rotate(90deg); +} + +/* Tool calls */ +.turn-tools { + margin: 0 16px 14px; + background: linear-gradient(135deg, #2a2a35 0%, #25252f 100%); + border: 1px solid #3a3a44; + border-radius: 8px; + padding: 12px 14px; + box-shadow: 0 2px 8px rgb(0, 0, 0, 0.2); +} + +.tool-calls-details { + cursor: pointer; + margin: 0; + padding: 0; +} + +.tool-calls-summary { + list-style: none; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 10px; + padding: 2px 0; + padding-inline-start: 0; + margin: 0; +} + +.tool-calls-summary::-webkit-details-marker { + display: none; +} + +.tool-calls-summary::marker { + display: none; +} + +.tool-calls-summary:hover { + color: #fff; +} + +.tools-header-inline { + font-size: 13px; + font-weight: 700; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.tool-summary-text { + font-size: 12px; + font-weight: 600; + color: #c084fc; + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.tool-summary-item { + background: rgb(192, 132, 252, 0.1); + border: 1px solid rgb(192, 132, 252, 0.3); + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; +} + +.tool-summary-item strong { + color: #e9d5ff; + font-weight: 700; +} + +.tools-header { + font-size: 13px; + font-weight: 700; + color: #fff; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.tools-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin-top: 10px; +} + +.tools-table thead th { + text-align: left; + padding: 8px 12px; + background: #1a1a22; + border-bottom: 2px solid #4a4a5a; + color: #94a3b8; + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.tools-table thead th:nth-child(2) { + text-align: right; +} + +.tools-table tbody .tool-row { + border-bottom: 1px solid #3a3a44; +} + +.tools-table tbody .tool-row:last-child { + border-bottom: none; +} + +.tool-name-cell { + padding: 10px 12px; + vertical-align: top; +} + +.tool-action-cell { + padding: 10px 12px; + text-align: right; + vertical-align: top; + width: 100px; +} + +.tool-name { + font-weight: 700; + color: #c084fc; + font-size: 13px; +} + +.tool-call-pretty { + font-weight: 700; + color: #34d399; + font-size: 12px; + text-decoration: underline; + white-space: nowrap; +} + +.tool-call-pretty:hover { + color: #6ee7b7; +} + +.tool-details { + margin-top: 8px; + font-size: 12px; +} + +.tool-details summary { + cursor: pointer; + color: #94a3b8; + font-weight: 600; +} + +.tool-details summary:hover { + color: #cbd5e1; +} + +.tool-details pre { + background: #1a1a20; + border: 1px solid #2a2a30; + padding: 10px; + border-radius: 6px; + overflow-x: auto; + max-height: 200px; + font-size: 11px; + margin-top: 6px; + line-height: 1.5; +} + +/* MCP tools */ +.turn-mcp { + margin: 0 16px 14px; + background: linear-gradient(135deg, #1e2e1e 0%, #1a261a 100%); + border: 1px solid #3a5a3a; + border-radius: 8px; + padding: 14px; + box-shadow: 0 2px 8px rgb(0, 0, 0, 0.2); +} + +.mcp-header { + font-size: 13px; + font-weight: 700; + color: #fff; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.mcp-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.mcp-item { + background: rgb(34, 197, 94, 0.1); + border: 1px solid rgb(34, 197, 94, 0.3); + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + color: #cbd5e1; +} + +.mcp-server { + font-weight: 600; + color: #22c55e; +} + +/* Context References - detail panel */ +.turn-context-details { + margin-bottom: 14px; + background: linear-gradient(135deg, #2a2535 0%, #252530 100%); + border: 1px solid #4a4a5a; + border-radius: 8px; + padding: 14px; + box-shadow: 0 2px 8px rgb(0, 0, 0, 0.2); +} + +.context-header { + font-size: 13px; + font-weight: 700; + color: #fff; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.context-section { + font-size: 13px; + color: #cbd5e1; + margin-bottom: 8px; + line-height: 1.6; +} + +.context-section:last-child { + margin-bottom: 0; +} + +.context-section strong { + color: #94a3b8; + font-weight: 600; +} + +.context-refs-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin-top: 8px; +} + +.context-refs-table th, +.context-refs-table td { + padding: 8px 12px; + text-align: left; + border-bottom: 1px solid #334155; +} + +.context-refs-table th { + color: #94a3b8; + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + background: #1e293b; +} + +.context-refs-table td { + color: #cbd5e1; +} + +.context-refs-table tbody tr:hover { + background: #1e293b; +} + +.context-refs-table .count-cell { + text-align: center; + font-weight: 600; + color: #60a5fa; +} + +.context-type-implicit { + color: #94a3b8; + font-size: 12px; +} + +.context-type-explicit { + color: #4ade80; + font-size: 12px; +} + +.context-path { + padding-left: 10px; + color: #9ca3af; + font-size: 12px; + margin-top: 4px; +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: #94a3b8; + font-size: 16px; + background: linear-gradient(135deg, #1a1a22 0%, #1f1f28 100%); + border: 1px solid #3a3a44; + border-radius: 12px; + box-shadow: 0 4px 12px rgb(0, 0, 0, 0.3); +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: #1a1a22; +} + +::-webkit-scrollbar-thumb { + background: #3a3a44; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: #4a4a54; +} + +/* Context References - collapsible panel */ +.turn-context-refs { + margin: 0 16px 14px; + background: linear-gradient(135deg, #2a2535 0%, #252530 100%); + border: 1px solid #4a4a5a; + border-radius: 8px; + padding: 12px 14px; + box-shadow: 0 2px 8px rgb(0, 0, 0, 0.2); +} + +.context-refs-details { + cursor: pointer; + margin: 0; + padding: 0; +} + +.context-refs-summary { + list-style: none; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 10px; + padding: 2px 0; + padding-inline-start: 0; + margin: 0; +} + +.context-refs-summary::-webkit-details-marker { + display: none; +} + +.context-refs-summary::marker { + display: none; +} + +.context-refs-summary:hover { + color: #fff; +} + +.context-refs-header-inline { + font-size: 13px; + font-weight: 700; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.context-ref-summary-text { + font-size: 12px; + font-weight: 600; + color: #22d3ee; + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.context-ref-item { + background: rgb(34, 211, 238, 0.1); + border: 1px solid rgb(34, 211, 238, 0.3); + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; +} + +.context-ref-item strong { + color: #a5f3fc; + font-weight: 700; +} + +.context-ref-implicit { + background: rgb(148, 163, 184, 0.1); + border: 1px solid rgb(148, 163, 184, 0.3); + color: #94a3b8; +} + +.context-ref-implicit strong { + color: #cbd5e1; +} + +.context-refs-content { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid rgb(255, 255, 255, 0.1); +} + +/* Footer */ +.footer { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #2a2a30; + font-size: 11px; + color: #666; +} diff --git a/src/webview/shared/contextRefUtils.ts b/src/webview/shared/contextRefUtils.ts new file mode 100644 index 0000000..89d18bb --- /dev/null +++ b/src/webview/shared/contextRefUtils.ts @@ -0,0 +1,81 @@ +/** + * Shared utilities for working with context references across webviews + */ + +export type ContextReferenceUsage = { + file: number; + selection: number; + implicitSelection: number; + symbol: number; + codebase: number; + workspace: number; + terminal: number; + vscode: number; + byKind: { [kind: string]: number }; + copilotInstructions: number; + agentsMd: number; + byPath: { [path: string]: number }; +}; + +/** + * Calculate the total number of context references. + * This is the single source of truth for what constitutes a context reference. + */ +export function getTotalContextRefs(refs: ContextReferenceUsage): number { + return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + + refs.workspace + refs.terminal + refs.vscode + refs.copilotInstructions + refs.agentsMd; +} + +/** + * Calculate the count of implicit (auto-attached) context references. + * Implicit refs are not user-initiated: copilotInstructions, agentsMd, implicitSelection + */ +export function getImplicitContextRefs(refs: ContextReferenceUsage): number { + return refs.copilotInstructions + refs.agentsMd + refs.implicitSelection; +} + +/** + * Calculate the count of explicit (user-initiated) context references. + * Explicit refs are user-initiated: #file, #selection, #symbol, #codebase, @workspace, @terminal, @vscode + */ +export function getExplicitContextRefs(refs: ContextReferenceUsage): number { + return refs.file + refs.selection + refs.symbol + refs.codebase + + refs.workspace + refs.terminal + refs.vscode; +} + +/** + * Generate a summary string of context references. + * @param refs - The context reference usage counts + * @param abbreviated - If true, use short labels (e.g., '#sel' instead of '#selection') + */ +export function getContextRefsSummary(refs: ContextReferenceUsage, abbreviated = false): string { + const parts: string[] = []; + + if (abbreviated) { + // Abbreviated labels for compact display (used in diagnostics) + if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } + if (refs.selection > 0) { parts.push(`#sel: ${refs.selection}`); } + if (refs.implicitSelection > 0) { parts.push(`impl: ${refs.implicitSelection}`); } + if (refs.symbol > 0) { parts.push(`#sym: ${refs.symbol}`); } + if (refs.codebase > 0) { parts.push(`#cb: ${refs.codebase}`); } + if (refs.workspace > 0) { parts.push(`@ws: ${refs.workspace}`); } + if (refs.terminal > 0) { parts.push(`@term: ${refs.terminal}`); } + if (refs.vscode > 0) { parts.push(`@vsc: ${refs.vscode}`); } + if (refs.copilotInstructions > 0) { parts.push(`📋 inst: ${refs.copilotInstructions}`); } + if (refs.agentsMd > 0) { parts.push(`🤖 ag: ${refs.agentsMd}`); } + } else { + // Full labels for detailed display (used in logviewer) + if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } + if (refs.selection > 0) { parts.push(`#selection: ${refs.selection}`); } + if (refs.implicitSelection > 0) { parts.push(`implicit: ${refs.implicitSelection}`); } + if (refs.symbol > 0) { parts.push(`#symbol: ${refs.symbol}`); } + if (refs.codebase > 0) { parts.push(`#codebase: ${refs.codebase}`); } + if (refs.workspace > 0) { parts.push(`@workspace: ${refs.workspace}`); } + if (refs.terminal > 0) { parts.push(`@terminal: ${refs.terminal}`); } + if (refs.vscode > 0) { parts.push(`@vscode: ${refs.vscode}`); } + if (refs.copilotInstructions > 0) { parts.push(`📋 instructions: ${refs.copilotInstructions}`); } + if (refs.agentsMd > 0) { parts.push(`🤖 agents: ${refs.agentsMd}`); } + } + + return parts.length > 0 ? parts.join(', ') : 'None'; +} diff --git a/src/webview/usage/main.ts b/src/webview/usage/main.ts index e1c2d8d..2ce194b 100644 --- a/src/webview/usage/main.ts +++ b/src/webview/usage/main.ts @@ -1,22 +1,11 @@ // Usage Analysis webview import { el } from '../shared/domUtils'; import { buttonHtml } from '../shared/buttonConfig'; +import { ContextReferenceUsage, getTotalContextRefs } from '../shared/contextRefUtils'; +// CSS imported as text via esbuild +import styles from './styles.css'; type ModeUsage = { ask: number; edit: number; agent: number }; -type ContextReferenceUsage = { - file: number; - selection: number; - implicitSelection: number; - symbol: number; - codebase: number; - workspace: number; - terminal: number; - vscode: number; - byKind: { [kind: string]: number }; - copilotInstructions: number; - agentsMd: number; - byPath: { [path: string]: number }; -}; type ToolCallUsage = { total: number; byTool: { [key: string]: number } }; type McpToolUsage = { total: number; byServer: { [key: string]: number }; byTool: { [key: string]: number } }; @@ -71,16 +60,6 @@ function escapeHtml(text: string): string { .replace(/'/g, '''); } -function getTotalContextRefs(refs: ContextReferenceUsage): number { - const basicRefs = refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + - refs.workspace + refs.terminal + refs.vscode; - - // Add contentReferences counts - const contentRefs = refs.copilotInstructions + refs.agentsMd; - - return basicRefs + contentRefs; -} - import toolNames from '../../toolNames.json'; let TOOL_NAME_MAP: { [key: string]: string } | null = toolNames || null; @@ -139,126 +118,7 @@ function renderLayout(stats: UsageAnalysisStats): void { const monthTotalModes = stats.month.modeUsage.ask + stats.month.modeUsage.edit + stats.month.modeUsage.agent; root.innerHTML = ` - +
diff --git a/src/webview/usage/styles.css b/src/webview/usage/styles.css new file mode 100644 index 0000000..e3401da --- /dev/null +++ b/src/webview/usage/styles.css @@ -0,0 +1,235 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0e0e0f; + color: #e7e7e7; + padding: 16px; + line-height: 1.5; + min-width: 320px; +} + +.container { + background: linear-gradient(135deg, #1b1b1e 0%, #1f1f22 100%); + border: 1px solid #2e2e34; + border-radius: 10px; + padding: 16px; + box-shadow: 0 4px 10px rgb(0, 0, 0, 0.28); + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 14px; + padding-bottom: 4px; +} + +.header-left { + display: flex; + align-items: center; + gap: 8px; +} + +.header-icon { + font-size: 20px; +} + +.header-title { + font-size: 16px; + font-weight: 700; + color: #fff; + letter-spacing: 0.2px; +} + +.button-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.nav-button { + background: #202024; + border: 1px solid #2d2d33; + color: #e7e7e7; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; +} + +.nav-button:hover { + background: #2a2a30; +} + +.nav-button.primary { + background: #0e639c; + border-color: #17b; + color: #fff; +} + +.nav-button.primary:hover { + background: #17b; +} + +.section { + background: #1b1b1e; + border: 1px solid #2a2a30; + border-radius: 8px; + padding: 12px; + margin-bottom: 16px; + box-shadow: 0 2px 6px rgb(0, 0, 0, 0.24); +} + +.section-title { + font-size: 14px; + font-weight: 700; + color: #fff; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 6px; + letter-spacing: 0.2px; +} + +.section-subtitle { + font-size: 12px; + color: #b8b8b8; + margin-bottom: 12px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.stat-card { + background: #18181b; + border: 1px solid #2a2a30; + border-radius: 6px; + padding: 12px; + box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2); +} + +.stat-card[title] { + cursor: help; +} + +.stat-label { + font-size: 11px; + color: #b8b8b8; + margin-bottom: 4px; +} + +.stat-value { + font-size: 20px; + font-weight: 700; + color: #f6f6f6; +} + +.bar-chart { + background: #18181b; + border: 1px solid #2a2a30; + border-radius: 6px; + padding: 12px; + margin-bottom: 12px; +} + +.bar-item { + margin-bottom: 8px; +} + +.bar-label { + display: flex; + justify-content: space-between; + font-size: 12px; + margin-bottom: 4px; + color: #d0d0d0; +} + +.bar-track { + background: #242429; + height: 8px; + border-radius: 4px; + overflow: hidden; +} + +.bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.list { + background: #18181b; + border: 1px solid #2a2a30; + border-radius: 6px; + padding: 12px 16px; +} + +.list ul { + list-style: none; + padding: 0; +} + +.list li { + padding: 4px 0; + font-size: 13px; +} + +.two-column { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.three-column { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 16px; +} + +.info-box { + background: #1b1b1e; + border: 1px solid #2a2a30; + border-radius: 6px; + padding: 12px; + margin-bottom: 16px; + font-size: 12px; + color: #c8c8c8; +} + +.info-box-title { + font-weight: 600; + color: #fff; + margin-bottom: 6px; +} + +.footer { + margin-top: 6px; + padding-top: 12px; + border-top: 1px solid #2a2a30; + text-align: left; + font-size: 11px; + color: #a0a0a0; +} + +@media (width <= 768px) { + .two-column { + grid-template-columns: 1fr; + } + + .three-column { + grid-template-columns: 1fr; + } +}