diff --git a/db/migrations/003_grounding.sql b/db/migrations/003_grounding.sql index cf790b99..eefba816 100644 --- a/db/migrations/003_grounding.sql +++ b/db/migrations/003_grounding.sql @@ -179,3 +179,9 @@ CREATE INDEX IF NOT EXISTS review_comments_source_idx ALTER TABLE llm_providers DROP CONSTRAINT IF EXISTS llm_providers_api_format_check; ALTER TABLE llm_providers ADD CONSTRAINT llm_providers_api_format_check CHECK (api_format IN ('openai', 'anthropic', 'gemini', 'cloudflare-workers-ai', 'vertex')); + +ALTER TABLE file_reviews ADD COLUMN IF NOT EXISTS batch_size INTEGER; + +CREATE INDEX IF NOT EXISTS file_reviews_batch_size_idx + ON file_reviews (batch_size) + WHERE batch_size > 1; diff --git a/package-lock.json b/package-lock.json index b2c8434a..76e3e545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,20 +14,21 @@ "clsx": "^2.1.1", "hono": "^4.12.25", "jsonrepair": "^3.15.0", - "lenis": "^1.3.25", + "lenis": "^1.3.26", "lucide-react": "^1.8.0", "motion": "^12.42.2", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "postgres": "^3.4.9", - "react": "^19.2.5", - "react-dom": "^19.2.5", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-markdown": "^10.1.0", - "react-router-dom": "^7.18.1", + "react-router-dom": "^7.18.2", "recharts": "^3.8.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", + "sugar-high": "^2.0.0", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" }, @@ -38,17 +39,17 @@ "@testing-library/react": "^16.3.2", "@types/node": "^25.6.0", "@types/picomatch": "^4.0.3", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", "chalk": "^5.6.2", "concurrently": "^9.2.4", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-react-hooks": "^7.1.1", "jsdom": "^29.0.2", - "ora": "^9.4.0", + "ora": "^9.4.1", "prompts": "^2.4.2", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", @@ -467,6 +468,91 @@ } } }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260801.1.tgz", + "integrity": "sha512-wuJWbXpKvncJi1P0GKS+iYpN5tHdb7JPJJ/+6ZQe8zzovHHVMkLJPNBsgWpqeUhpM3g9qTwEKd2rglNKejuh5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260801.1.tgz", + "integrity": "sha512-kwoZiTpnhNrF3+APx84Q/oAqvJ3sU9yefGagwm/ASaH/2W19x0vghkW/r4qCoHCK0WW7EPugZ+aXjgPMRtlq1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260801.1.tgz", + "integrity": "sha512-r0vAxCZH+Jih9Unm1yoyiByPNWNgawcKciOHDm5Q37ZVGOkKLsT9AtLe3yLSaul76WrKqtf+JP2n0WW32VBLJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260801.1.tgz", + "integrity": "sha512-zWgpdZtSozvIgzQNmQiDSF8yEOQJUkRAWNsDXXzAAoy+fCn8YUoSibj3mpFSbZRvbUldeBEaW6SCdC2VEMkhNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260801.1.tgz", + "integrity": "sha512-2oQz+Ksu4ji6e/+ZoYX+tWQEcxAii2p7l+iR8kx48W1llMalaufAsmVxTlk+3/vrM7D3/2c0iK448e0UQTcIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2354,9 +2440,9 @@ } }, "node_modules/@speed-highlight/core": { - "version": "1.2.20", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.20.tgz", - "integrity": "sha512-biXn20UJkNJ9cB35lPXJKfgUl1NQfhbApt78oePVjC0gB4+hlJyjkUz1hoLHLk7dEl+mSbW0VtX4C2IlRIDoHA==", + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", + "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", "dev": true, "license": "CC0-1.0" }, @@ -2827,14 +2913,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.17", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.3", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3461,7 +3551,9 @@ ] }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -4028,6 +4120,8 @@ }, "node_modules/cookie": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" @@ -4369,9 +4463,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -5037,7 +5131,9 @@ } }, "node_modules/hono": { - "version": "4.12.27", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -5357,7 +5453,9 @@ } }, "node_modules/lenis": { - "version": "1.3.25", + "version": "1.3.26", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.26.tgz", + "integrity": "sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==", "license": "MIT", "workspaces": [ "packages/*", @@ -6509,6 +6607,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/miniflare": { + "version": "5.20260801.1-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260801.1-alpha.tgz", + "integrity": "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.29.0", + "workerd": "1.20260801.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -6565,7 +6681,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -6779,7 +6897,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -6789,7 +6909,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -6807,7 +6929,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6883,20 +7005,24 @@ } }, "node_modules/react": { - "version": "19.2.7", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-is": { @@ -6951,9 +7077,9 @@ } }, "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -6973,12 +7099,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.18.1" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -7452,6 +7578,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/sugar-high": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sugar-high/-/sugar-high-2.0.0.tgz", + "integrity": "sha512-qZQaa+3vbmKSx00G465bY1LqVUU0RaUkY/7xDV63cQzezD6dF/DJCthztRQqYLGA7HXdObvCEtcCRpvoIrrbCg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -7665,7 +7797,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -8172,10 +8306,31 @@ "node": ">=0.10.0" } }, + "node_modules/workerd": { + "version": "1.20260801.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260801.1.tgz", + "integrity": "sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260801.1", + "@cloudflare/workerd-darwin-arm64": "1.20260801.1", + "@cloudflare/workerd-linux-64": "1.20260801.1", + "@cloudflare/workerd-linux-arm64": "1.20260801.1", + "@cloudflare/workerd-windows-64": "1.20260801.1" + } + }, "node_modules/wrangler": { - "version": "4.118.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.118.0.tgz", - "integrity": "sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw==", + "version": "4.120.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.120.0.tgz", + "integrity": "sha512-cBmu/MeaB/fPacC0JpATs4duTOCagBxrZo+vBzuTX06tLzwSyAHE1drlHUZ8rP0VqVz1fy3ReGYTiHdKkoHltg==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -8183,10 +8338,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20260730.0-alpha", + "miniflare": "5.20260801.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260730.1" + "workerd": "1.20260801.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -8200,7 +8355,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260730.1" + "@cloudflare/workers-types": "^5.20260801.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -8208,91 +8363,6 @@ } } }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260730.1.tgz", - "integrity": "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260730.1.tgz", - "integrity": "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260730.1.tgz", - "integrity": "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260730.1.tgz", - "integrity": "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260730.1.tgz", - "integrity": "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/wrangler/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8308,45 +8378,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/wrangler/node_modules/miniflare": { - "version": "5.20260730.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260730.0-alpha.tgz", - "integrity": "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.35.2", - "undici": "7.28.0", - "workerd": "1.20260730.1", - "ws": "8.21.0", - "youch": "4.1.0-beta.10" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/wrangler/node_modules/workerd": { - "version": "1.20260730.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260730.1.tgz", - "integrity": "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260730.1", - "@cloudflare/workerd-darwin-arm64": "1.20260730.1", - "@cloudflare/workerd-linux-64": "1.20260730.1", - "@cloudflare/workerd-linux-arm64": "1.20260730.1", - "@cloudflare/workerd-windows-64": "1.20260730.1" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "dev": true, @@ -8403,6 +8434,8 @@ }, "node_modules/ws": { "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 40601dcd..18c5534f 100644 --- a/package.json +++ b/package.json @@ -36,17 +36,17 @@ "@testing-library/react": "^16.3.2", "@types/node": "^25.6.0", "@types/picomatch": "^4.0.3", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", "chalk": "^5.6.2", "concurrently": "^9.2.4", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-react-hooks": "^7.1.1", "jsdom": "^29.0.2", - "ora": "^9.4.0", + "ora": "^9.4.1", "prompts": "^2.4.2", "tailwindcss": "^4.2.2", "typescript": "^6.0.2", @@ -61,21 +61,27 @@ "clsx": "^2.1.1", "hono": "^4.12.25", "jsonrepair": "^3.15.0", - "lenis": "^1.3.25", + "lenis": "^1.3.26", "lucide-react": "^1.8.0", "motion": "^12.42.2", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "postgres": "^3.4.9", - "react": "^19.2.5", - "react-dom": "^19.2.5", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-markdown": "^10.1.0", - "react-router-dom": "^7.18.1", + "react-router-dom": "^7.18.2", "recharts": "^3.8.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", + "sugar-high": "^2.0.0", "tailwind-merge": "^3.5.0", "zod": "^4.3.6" + }, + "allowScripts": { + "esbuild": true, + "unrs-resolver@1.12.2": true, + "workerd@1.20260801.1": true } } diff --git a/public/assets/codra-dashboard.png b/public/assets/codra-dashboard.png index a2d82ee5..6b59d727 100644 Binary files a/public/assets/codra-dashboard.png and b/public/assets/codra-dashboard.png differ diff --git a/scripts/migrate.mjs b/scripts/migrate.mjs index e5c089a9..cc903577 100644 --- a/scripts/migrate.mjs +++ b/scripts/migrate.mjs @@ -276,11 +276,10 @@ async function main() { console.log('Starting database migrations...'); await query('BEGIN'); try { - // Transaction-scoped deliberately. A session-scoped `pg_advisory_lock` leaked in production: the - // process died before its `finally` unlock, the connection returned to the pooler still holding the - // lock, and every later migrate blocked forever until pg_terminate_backend. `pg_advisory_xact_lock` - // is released by Postgres on COMMIT/ROLLBACK/disconnect, and `SET LOCAL` avoids the same pooled - // session-state leak. + // Transaction-scoped on purpose: a session-scoped pg_advisory_lock once leaked in production + // when the process died before its `finally` unlock, leaving the pooler holding it and + // blocking every later migrate until pg_terminate_backend. pg_advisory_xact_lock and SET LOCAL + // release automatically on COMMIT/ROLLBACK/disconnect. console.log('Acquiring advisory lock...'); await query("SET LOCAL lock_timeout = '30s'"); await query('SELECT pg_advisory_xact_lock($1)', [migrationLockId]); @@ -312,8 +311,7 @@ async function main() { console.log('Database migrations are up to date.'); } finally { - // No unlock needed: COMMIT/ROLLBACK above released the transaction-scoped lock, and closing the - // connection would release it regardless. + // No explicit unlock needed: COMMIT/ROLLBACK already released the transaction-scoped lock. await sql.end(); } } diff --git a/scripts/setup-cloudflare.js b/scripts/setup-cloudflare.js index c27c7828..0fc2870e 100644 --- a/scripts/setup-cloudflare.js +++ b/scripts/setup-cloudflare.js @@ -18,8 +18,7 @@ async function main() { console.log(chalk.gray('This script will automatically configure your Cloudflare resources.\n')); const env = getEnvVars(); - - // 1. Prerequisites Check + const authSpinner = ora('Checking Cloudflare authentication...').start(); let globallyAuthenticated = true; try { @@ -42,7 +41,6 @@ async function main() { process.exit(1); } - // 2. KV Namespace console.log(chalk.cyan.bold('๐Ÿ“ฆ KV Namespaces')); const kvId = await handleKVNamespace('codra-review', false); if (!kvId) console.log(chalk.yellow(' โš ๏ธ Could not extract KV ID.')); @@ -51,7 +49,6 @@ async function main() { if (!kvPreviewId) console.log(chalk.yellow(' โš ๏ธ Could not extract preview KV ID.')); console.log(''); - // 3. Queues console.log(chalk.cyan.bold('๐Ÿ“จ Queues')); const jobsSpinner = ora('Creating jobs queue (codra-review-jobs)...').start(); try { @@ -68,7 +65,6 @@ async function main() { console.log(''); - // 4. Hyperdrive console.log(chalk.cyan.bold('๐Ÿ—„๏ธ Hyperdrive')); console.log(chalk.gray(` (Using default from .dev.vars if available)`)); const { dbUrl } = await prompts({ @@ -91,7 +87,6 @@ async function main() { const hyperdriveId = await handleHyperdrive(dbUrl); console.log(''); - // 5. Domain Configuration console.log(chalk.cyan.bold('๐ŸŒ Domain Configuration')); const { domainChoice } = await prompts({ type: 'select', @@ -133,7 +128,6 @@ async function main() { } console.log(''); - // 6. Application Variables console.log(chalk.cyan.bold('๐Ÿ“ Application Variables')); const { botUsername } = await prompts({ type: 'text', @@ -157,15 +151,12 @@ async function main() { }, { onCancel: () => process.exit(1) }); console.log(''); - // 7. Config Update console.log(chalk.cyan.bold('โš™๏ธ Configuration')); const configSpinner = ora('Updating wrangler.jsonc...').start(); let wranglerConfig = fs.readFileSync(WRANGLER_JSONC_PATH, 'utf-8'); let configChanged; - // Escape a string for safe embedding inside JSON double-quoted literals. - // Use JSON.stringify (which correctly escapes backslashes, quotes, and control - // characters) and strip the surrounding quotes it adds. + // JSON.stringify escapes backslashes/quotes/control chars correctly; strip the quotes it adds. const escapeJson = (str) => JSON.stringify(String(str)).slice(1, -1); const routeRegex = /"routes"\s*:\s*\[[\s\S]*?\]|"workers_dev"\s*:\s*(true|false)/; @@ -212,7 +203,6 @@ async function main() { } console.log(''); - // 8. Secrets console.log(chalk.cyan.bold('๐Ÿ” Secrets')); const requiredSecrets = [ "APP_PRIVATE_KEY", diff --git a/scripts/setup-helpers.js b/scripts/setup-helpers.js index 312e3382..d7b3bcc5 100644 --- a/scripts/setup-helpers.js +++ b/scripts/setup-helpers.js @@ -2,9 +2,9 @@ import { exec, spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -// Pure-ish leaves of the Cloudflare setup script: process spawning, id extraction, .dev.vars -// reading and secret writing. None of them holds a handle open across calls, so importing this -// module has no side effects -- the imperative provisioning flow stays in setup-cloudflare.js. +// Pure-ish leaves of the Cloudflare setup script (process spawning, id extraction, .dev.vars +// I/O). No handles held across calls, so importing this has no side effects; the imperative +// provisioning flow stays in setup-cloudflare.js. export const WRANGLER_JSONC_PATH = path.join(process.cwd(), 'wrangler.jsonc'); export const DEV_VARS_PATH = path.join(process.cwd(), '.dev.vars'); @@ -41,8 +41,7 @@ export function getEnvVars() { if (line.trim() && !line.startsWith('#')) { const [key, ...values] = line.split('='); if (key && values.length > 0) { - // Strip surrounding quotes, then unescape literal \n sequences - // (wrangler secrets must receive real newlines, not the two chars \ and n) + // Unescape literal \n: wrangler secrets need real newlines, not the two chars \ and n. const raw = values.join('=').trim().replace(/^"|"$/g, ''); env[key.trim()] = raw.replace(/\\n/g, '\n'); } diff --git a/scripts/setup-provisioning.js b/scripts/setup-provisioning.js index ec715681..34ddaf02 100644 --- a/scripts/setup-provisioning.js +++ b/scripts/setup-provisioning.js @@ -7,8 +7,8 @@ import { extractId, spawnAsync } from './setup-helpers.js'; const execAsync = util.promisify(exec); -// The two resource-provisioning steps, each idempotent: reuse the binding already in wrangler.jsonc -// if it resolves, otherwise create it and write the id back. Called in order by main(). +// Idempotent provisioning: reuse the binding already in wrangler.jsonc if it resolves, +// otherwise create it and write the id back. export async function handleKVNamespace(baseBinding, isPreview) { let currentBinding = baseBinding; diff --git a/src/client/app.css b/src/client/app.css index e53a2c05..5a734f86 100644 --- a/src/client/app.css +++ b/src/client/app.css @@ -736,13 +736,15 @@ } .dark .app-shell-content { - --background: oklch(18% 0.018 115); - --card: oklch(18% 0.018 115); - --muted: oklch(22% 0.02 115); - --popover: oklch(18% 0.018 115); - --secondary: oklch(26% 0.02 115); - --border: oklch(22% 0.02 115); - --input: oklch(22% 0.02 115); + /* Zinc, matching the light block's hue: a cool neutral (hue 286.3, low + chroma) rather than the warm olive cast that hue 115 gave the card. */ + --background: oklch(18% 0.006 286.3); + --card: oklch(18% 0.006 286.3); + --muted: oklch(22% 0.006 286.3); + --popover: oklch(18% 0.006 286.3); + --secondary: oklch(26% 0.007 286.3); + --border: oklch(22% 0.006 286.3); + --input: oklch(22% 0.006 286.3); } /* Sidebar rows: the SharedLayoutBg pill is the sole hover affordance - the @@ -823,20 +825,31 @@ background: oklch(19% 0 0); } -/* Diff-viewer syntax tokens (see src/client/lib/highlight.tsx). Muted, - theme-aware hues so highlighted code stays readable on the add/del tints. */ -.tok-kw { color: oklch(48% 0.19 305); } -.tok-str { color: oklch(46% 0.12 150); } -.tok-num { color: oklch(50% 0.13 65); } -.tok-com { color: oklch(58% 0.01 260); font-style: italic; } -.tok-fn { color: oklch(46% 0.14 260); } -.tok-type { color: oklch(45% 0.11 200); } -.dark .tok-kw { color: oklch(75% 0.14 305); } -.dark .tok-str { color: oklch(76% 0.11 150); } -.dark .tok-num { color: oklch(78% 0.12 65); } -.dark .tok-com { color: oklch(58% 0.01 260); } -.dark .tok-fn { color: oklch(76% 0.1 260); } -.dark .tok-type { color: oklch(78% 0.1 200); } +/* Diff-viewer syntax tokens (sugar-high, see src/client/lib/highlight.tsx). Muted, + theme-aware hues so highlighted code stays readable on the add/del tints. + sugar-high emits `color: var(--sh-)` per token. */ +:root { + --sh-keyword: oklch(48% 0.19 305); + --sh-string: oklch(46% 0.12 150); + --sh-class: oklch(50% 0.13 65); + --sh-comment: oklch(58% 0.01 260); + --sh-entity: oklch(46% 0.14 260); + --sh-property: oklch(45% 0.11 200); + --sh-identifier: inherit; + --sh-sign: oklch(58% 0.01 260); + --sh-jsxliterals: inherit; + --sh-break: inherit; + --sh-space: inherit; +} +.dark { + --sh-keyword: oklch(75% 0.14 305); + --sh-string: oklch(76% 0.11 150); + --sh-class: oklch(78% 0.12 65); + --sh-comment: oklch(58% 0.01 260); + --sh-entity: oklch(76% 0.1 260); + --sh-property: oklch(78% 0.1 200); +} +.sh__token--comment { font-style: italic; } /* Diff row tints + add/del number colors (theme vars set above). */ .diff-add { background-color: var(--diff-add-bg); } @@ -844,12 +857,6 @@ .diff-add-fg { color: var(--diff-add-fg); } .diff-del-fg { color: var(--diff-del-fg); } -/* Markdown inline tokens (.md diffs). */ -.tok-md-head { color: oklch(46% 0.14 260); font-weight: 600; } -.tok-strong { color: inherit; font-weight: 700; } -.tok-em { color: inherit; font-style: italic; } -.dark .tok-md-head { color: oklch(76% 0.1 260); } - /* Late-arriving header/meta content eases in instead of popping. */ @keyframes ui-fade-in { from { opacity: 0; transform: translateY(2px); } diff --git a/src/client/components/features/account/detail-rows.tsx b/src/client/components/features/account/detail-rows.tsx index c738d938..f6870c31 100644 --- a/src/client/components/features/account/detail-rows.tsx +++ b/src/client/components/features/account/detail-rows.tsx @@ -3,10 +3,7 @@ import { cn } from '@client/lib/utils'; import { LayerCard } from '@client/components/ui/layer-card'; import { Text } from '@client/components/ui/text'; import { Skeleton } from '@client/components/shared/skeleton'; -// The labelled key/value rows the account page is built from, plus the click-to-reveal -// wrapper used for values that should not sit on screen by default. -/* โ”€โ”€ A captioned group of key/value rows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ export function DetailGroup({ caption, children }: { caption: string; children: React.ReactNode }) { return (
@@ -18,7 +15,6 @@ export function DetailGroup({ caption, children }: { caption: string; children: ); } -/* โ”€โ”€ A value that stays blurred until clicked, with a hover tooltip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ export function RevealOnClick({ label, children }: { label: string; children: React.ReactNode }) { const [revealed, setRevealed] = useState(false); const hint = revealed ? 'Click to hide' : 'Click to reveal'; @@ -43,8 +39,7 @@ export function RevealOnClick({ label, children }: { label: string; children: Re {children} - {/* Tooltip - shown on hover/focus of the group. `pointer-events-none` so it - can never sit between the cursor and the button underneath it. */} + {/* `pointer-events-none` so the tooltip can never sit between the cursor and the button underneath it. */} - {/* Label is secondary: near-black in light, recessed in dark. */} {label} diff --git a/src/client/components/features/job-detail/comment-card.tsx b/src/client/components/features/job-detail/comment-card.tsx index 327d0c53..42c9c17b 100644 --- a/src/client/components/features/job-detail/comment-card.tsx +++ b/src/client/components/features/job-detail/comment-card.tsx @@ -33,9 +33,8 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { const sev = severityConfig[comment.severity] ?? severityConfig.nit; const SevIcon = sev.icon; - // Optimistic, and reverted on failure. Labelling is the only path by which ground truth ever - // reaches this system, so it has to be one click with no dialog -- but a 'wrong' label suppresses - // the finding across the whole repository, so the button says so before it is pressed. + // Optimistic, reverted on failure; a "wrong" label suppresses the finding repo-wide, so the + // button warns about that before it's pressed. const [label, setLabel] = useState(comment.humanLabel ?? null); const [saving, setSaving] = useState(false); @@ -57,8 +56,7 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { const canLabel = Boolean(jobId && comment.fingerprint); - // Plain code rendering (no syntax highlighting) -- findings read better as - // quiet monospace blocks against the tinted severity card. + // No syntax highlighting: findings read better as quiet monospace blocks on the tinted severity card. const markdownComponents = { code({ className, children, ...props }: ComponentPropsWithoutRef<'code'> & { className?: string }) { const text = String(children ?? ''); @@ -80,7 +78,6 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { return (
- {/* Header row */}
{sev.svg ? ( @@ -93,7 +90,6 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { {comment.severity}
- {/* Meta: file ยท line */}
{filePath} @@ -106,9 +102,7 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { {comment.claimType.replace(/_/g, ' ')} )} - {/* A finding in this list did not necessarily reach the pull request -- the dashboard shows - everything the model produced. Say which stage stopped it rather than leaving a filtered - finding looking identical to a posted one. */} + {/* Say which stage stopped it rather than leaving a filtered finding looking identical to a posted one. */} {comment.posted === false && comment.disposition && comment.disposition !== 'posted' && ( - {/* The verifier's own words. Parsed and thrown away until now, which made the one stage whose - job is subtraction the one stage nobody could tune. */} + {/* The verifier's own words - previously parsed and thrown away, leaving nothing to tune this stage against. */} {comment.verifyReason && (

Verifier: {comment.verifyReason}

)} - {/* Body - stripped of suggestions to avoid duplication in UI */}
{comment.body.split('```suggestion')[0].trim()}
- {/* The diff context the claim was judged against, captured at parse time. Collapsed so it - doesn't dominate the card, but present: a human cannot label a finding they cannot see, and - this is the only surviving copy (migration 003 nulls diff_input, the KV cache is 6h). */} + {/* Collapsed, not removed: migration 003 nulls diff_input and the KV cache is only 6h, so this + is the last surviving copy of the diff context once judged. */} {comment.contextSnippet && (
)} - {/* Code suggestion (UI view) */} {comment.codeSuggestion && (
diff --git a/src/client/components/features/job-detail/diff-file-panel.tsx b/src/client/components/features/job-detail/diff-file-panel.tsx index 1bbf4ac9..9ff39061 100644 --- a/src/client/components/features/job-detail/diff-file-panel.tsx +++ b/src/client/components/features/job-detail/diff-file-panel.tsx @@ -6,22 +6,18 @@ import { highlightLine, langForPath } from '@client/lib/highlight'; import { cn } from '@client/lib/utils'; import type { FileReviewRecord, ParsedReviewComment } from '@shared/schema'; import { CommentCard } from './comment-card'; -// One expandable file panel in the diff view, and the single diff row it repeats. Holds the -// row-height and preview-size constants because they only mean anything here. export const LARGE_DIFF_ROWS = 300; -/* Longer diffs render only the first PREVIEW_ROWS lines behind a "Show full diff" control, so a huge - PR never dumps tens of thousands of rows into the DOM. Files carrying review comments are never - truncated, since their anchors must stay visible. */ +// Longer diffs render only the first PREVIEW_ROWS lines behind a "Show full diff" control, so a huge +// PR never dumps tens of thousands of rows into the DOM. Files with comments are never truncated. export const PREVIEW_ROWS = 150; -/* Row height used to estimate a panel's rendered size before it first paints. */ +// Estimated row height, used to size a panel before it first paints. export const DIFF_ROW_PX = 20; -/* Offscreen panels skip layout and paint; the placeholder height keeps the scrollbar honest. A - per-file estimate from the diff's own line count keeps page height stable while scrolling instead - of "growing" as panels come into view. */ +// Offscreen panels skip layout/paint; this placeholder height keeps the scrollbar and page height +// stable instead of the page "growing" as panels come into view. export function panelCvStyle(open: boolean, lineEstimate: number): CSSProperties { const body = open ? Math.min(lineEstimate, PREVIEW_ROWS) * DIFF_ROW_PX + 140 : 0; return { @@ -55,8 +51,7 @@ export function DiffLine({ row, lang }: { row: DiffRow; lang: ReturnType langForPath(file.filePath), [file.filePath]); - // Header stats come from a cheap line scan; the full row parse only happens once - // the panel is actually open - collapsed files cost almost nothing. + // Header stats come from a cheap line scan; the full row parse only happens once the panel opens. const { adds, dels } = useMemo(() => diffStats(file.diffInput), [file.diffInput]); const rows = useMemo( () => (open && file.diffInput ? parsePromptDiff(file.diffInput) : NO_ROWS), [open, file.diffInput], ); - // GitHub-style truncation: long diffs render a preview with a "Show full diff" - // control. Files with review comments always render fully (anchors must show). + // Files with review comments always render fully, since their anchors must stay visible. const truncatable = rows.length > LARGE_DIFF_ROWS && file.parsedComments.length === 0; const [showFull, setShowFull] = useState(false); const visibleRows = useMemo( @@ -106,8 +99,7 @@ export function FileDiff({ file, open, viewed, diffsLoading = false, onOpenChang ); const hiddenLines = rows.length - visibleRows.length; - // Anchor comments to their new-file line and split the diff into segments: - // runs of rows, interrupted by comment blocks. Unmatched comments fall to the end. + // Unmatched comments (no new-file line, or line not in the visible rows) fall to the end. const { segments, unanchored } = useMemo(() => { const byLine = new Map(); const rest: ParsedReviewComment[] = []; @@ -149,7 +141,6 @@ export function FileDiff({ file, open, viewed, diffsLoading = false, onOpenChang return (
- {/* File header */}
- {/* Diff body */} {open && (
{rows.length === 0 ? ( @@ -212,9 +202,8 @@ export function FileDiff({ file, open, viewed, diffsLoading = false, onOpenChang {diffsLoading ? 'Loading diffโ€ฆ' : 'Diff unavailable for this file.'}

) : ( - // Comments split the diff into independently-scrollable row segments so - // comment cards stay at panel width instead of stretching to the widest - // code line inside one shared horizontal scroller. + // Each segment scrolls independently, so comment cards stay at panel width instead of + // stretching to the widest code line in a shared scroller. segments.map((segment, i) => segment.type === 'rows' ? (
@@ -236,7 +225,6 @@ export function FileDiff({ file, open, viewed, diffsLoading = false, onOpenChang ) )} - {/* GitHub-style truncation footer for long diffs */} {hiddenLines > 0 && (

@@ -263,7 +251,6 @@ export function FileDiff({ file, open, viewed, diffsLoading = false, onOpenChang

)} - {/* Error + comments that didn't match a diff line */} {file.fileStatus === 'failed' && file.errorMessage && (
{nodes.map((node) => { @@ -84,8 +82,7 @@ export function FileTree({ nodes, collapsedDirs, viewedFiles, selectedFileId, on {node.file.parsedComments.length > 0 && ( )} - {/* Counts appear only on hover/selection - `hidden` (not opacity-0) so - they don't reserve width and squeeze the filename when invisible. */} + {/* `hidden` not opacity-0, so counts don't reserve width and squeeze the filename when invisible. */} - {/* Fixed 48px summary row, matching the jobs table's row height: mono path - on the left, then the status dot + verdict pill + finding count. */} - {/* `select-none` used to sit here, which made the file path -- the one thing in this row worth - copying -- impossible to select at all. Selection is allowed instead, and the click guard - keeps a drag-select from collapsing the panel on mouse-up. */} + {/* Selection is allowed (not `select-none`) so the file path can be copied; this guard just + stops a drag-select from also collapsing the panel on mouse-up. */}
- {/* File-level error */} {file.fileStatus === 'failed' && file.errorMessage && (

@@ -68,7 +64,6 @@ export function FileFinding({ file }: FileFindingProps) {

)} - {/* File summary (when review succeeded) */} {file.fileStatus === 'done' && file.fileSummary && (

diff --git a/src/client/components/features/job-detail/job-chips.tsx b/src/client/components/features/job-detail/job-chips.tsx index 8a8c9eeb..90f75063 100644 --- a/src/client/components/features/job-detail/job-chips.tsx +++ b/src/client/components/features/job-detail/job-chips.tsx @@ -1,7 +1,6 @@ /** - * The row vocabulary shared by the job detail page, mirroring the jobs table: a coloured status dot - * plus word and duration, restrained bordered verdict pills, and compact muted icon chips for - * everything else. One place, so the detail page and the list read as one product. + * Row vocabulary shared by the job detail page, mirroring the jobs table so the two read as one + * product: status dot + word + duration, bordered verdict pills, muted icon chips. */ import { useState, type ReactNode } from 'react'; import { CheckCircle2, MessageSquare, type LucideIcon } from 'lucide-react'; @@ -15,16 +14,13 @@ export { formatRelativeDate, formatRunDuration, jobDuration, statusLabel } from import type { JobDetail, JobSummary } from '@shared/schema'; -/** - * Full stamp for `title` tooltips, so the terse relative text stays precise. - * Rendered in the account's display time zone (falls back to UTC, not the browser's). - */ +/** Full stamp for `title` tooltips, in the account's display time zone (falling back to UTC). */ export function formatAbsoluteDate(value: string | Date | null | undefined) { if (!value) return undefined; const date = new Date(value); if (Number.isNaN(date.getTime())) return undefined; - // Component options, NOT dateStyle/timeStyle: Intl throws `Invalid option` if - // either style shorthand is combined with a component like `timeZoneName`. + // Component options, not dateStyle/timeStyle: Intl throws if a style shorthand is combined + // with a component option like `timeZoneName`. return formatDateTime(date, { year: 'numeric', month: 'short', @@ -35,9 +31,7 @@ export function formatAbsoluteDate(value: string | Date | null | undefined) { }); } -/* โ”€โ”€ Pieces โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - -/** The 7px status dot on its own (for rows that carry their own label). */ +/** Status dot alone, for rows that render their own label. */ export function StatusDot({ status, className }: { status: string; className?: string }) { return ( ; } -/** - * Bordered verdict pill with a tinted leading icon: the border stays neutral so - * only the icon carries colour. - */ +/** Verdict pill: the border stays neutral so only the leading icon carries colour. */ export function VerdictPill({ verdict }: { verdict: NonNullable }) { const approved = verdict === 'approve'; const Icon = approved ? CheckCircle2 : MessageSquare; @@ -151,9 +139,8 @@ export function MetaChip({ } /** - * Author avatar. Hits avatars.githubusercontent.com directly (github.com/.png - * only 302-redirects, and that hop can fail) and falls back to an initial, so the - * header never shows a broken-image glyph. No `loading="lazy"` - intersection + * Author avatar. Hits avatars.githubusercontent.com directly, since the github.com/.png + * redirect hop can fail, and falls back to an initial. No `loading="lazy"`: intersection * detection is unreliable inside the app's scroll containers. */ export function AuthorAvatar({ login, size = 20 }: { login: string | null; size?: number }) { @@ -184,7 +171,6 @@ export function AuthorAvatar({ login, size = 20 }: { login: string | null; size? ); } -/** Avatar + login as one chip, for the header's identity line. */ export function AuthorChip({ login }: { login: string | null }) { if (!login) return null; return ( @@ -197,28 +183,17 @@ export function AuthorChip({ login }: { login: string | null }) { ); } -/* โ”€โ”€ Row rhythm โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - -/** - * One label โ†’ value row inside a panel. Fixed height and hairline dividers give - * the detail cards the same uniform rhythm as the table's 48px rows (a touch - * tighter, since these are dense key/value pairs). - */ +/** One label โ†’ value row. Fixed height and hairline dividers echo the table's 48px rhythm. */ export const DETAIL_ROW = 'flex h-11 items-center justify-between gap-4 border-t border-ui-line first:border-transparent'; -/** Muted row label - quiet, sentence case, never competing with its value. */ export const DETAIL_LABEL = 'shrink-0 text-xs leading-none text-ui-default dark:text-ui-subtle'; -/** The dash placeholder used wherever a nullable field is absent. */ export function EmptyValue() { return -; } -/** - * A file path rendered like the table's mono cells: the directory prefix recedes - * and the basename carries the weight, so long paths stay scannable. - */ +/** File path in the table's mono style: the directory recedes, the basename carries the weight. */ export function MonoPath({ path, className }: { path: string; className?: string }) { const slash = path.lastIndexOf('/'); const dir = slash === -1 ? '' : path.slice(0, slash + 1); diff --git a/src/client/components/features/job-detail/job-diffs.tsx b/src/client/components/features/job-detail/job-diffs.tsx index edd0fd54..b5a66e6b 100644 --- a/src/client/components/features/job-detail/job-diffs.tsx +++ b/src/client/components/features/job-detail/job-diffs.tsx @@ -16,9 +16,7 @@ import type { FileReviewRecord, JobDetail } from '@shared/schema'; import { panelCvStyle, fileAnchorId, FileDiff } from './diff-file-panel'; import { FileTree } from './diff-file-tree'; -/** A file present in the PR diff that has no review row (yet) - e.g. the job is - still running, or the file was skipped. Shown like GitHub shows every changed - file, with a "pending" status instead of review results. */ +/** A file present in the PR diff with no review row yet (job still running or file skipped) - shown as pending, GitHub-style. */ function syntheticFileReview(jobId: string, filePath: string, diffInput: string): FileReviewRecord { return { id: `diff-only:${filePath}`, @@ -40,13 +38,9 @@ function syntheticFileReview(jobId: string, filePath: string, diffInput: string) }; } -/* Above this many files, only files with review comments start expanded - - the tree is the navigation surface, so a huge PR opens as a short page. */ +// Above this many files, only files with review comments start expanded, so a huge PR opens short. const AUTO_EXPAND_FILE_LIMIT = 8; - -/* โ”€โ”€ Files-changed view โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ - interface JobDiffsProps { job: JobDetail; } @@ -75,9 +69,7 @@ export function JobDiffs({ job }: JobDiffsProps) { }; }, [job.id]); - // The full GitHub-style file list: every file in the PR diff, merged with review - // rows where they exist. Files without a review row yet (job still running, - // skipped, etc.) show as pending, sorted by path like GitHub. + // Every file in the PR diff merged with review rows where they exist, sorted by path like GitHub. const files = useMemo(() => { const merged = job.files.map((f) => diffsByPath?.[f.filePath] ? { ...f, diffInput: diffsByPath[f.filePath] } : f, @@ -91,8 +83,7 @@ export function JobDiffs({ job }: JobDiffsProps) { return merged.sort((a, b) => a.filePath.localeCompare(b.filePath)); }, [job.id, job.files, diffsByPath]); - // Per-file cheap stats, computed once per diff change and reused everywhere - // (header counts, tree, totals, panel size estimates). + // Computed once per diff change and reused everywhere (header counts, tree, totals, panel size estimates). const statsByPath = useMemo(() => { const map = new Map>(); for (const file of files) map.set(file.filePath, diffStats(file.diffInput)); @@ -100,9 +91,7 @@ export function JobDiffs({ job }: JobDiffsProps) { }, [files]); const [viewedFiles, setViewedFiles] = useState>(() => new Set()); - // Open state is an override on top of a default rule, so files that stream in - // later (running jobs, late-arriving diffs) still get sensible defaults: big - // PRs start collapsed except files carrying review comments. + // Open state is an override on top of a default rule, so late-arriving files still get sensible defaults. const [openOverrides, setOpenOverrides] = useState>(() => new Map()); const isLargePr = files.length > AUTO_EXPAND_FILE_LIMIT; const isOpen = (file: FileReviewRecord) => @@ -141,13 +130,11 @@ export function JobDiffs({ job }: JobDiffsProps) { setSelectedFileId(file.id); setOpen(file.id, true); if (singleFileMode) return; // the single panel swaps in place, nothing to scroll to - // Let the panel expand before scrolling to it. requestAnimationFrame(() => { fileRefs.current.get(file.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }; - // Single-file mode current selection (defaults to the first file). const currentIndex = Math.max(0, files.findIndex((f) => f.id === selectedFileId)); const currentFile = files[currentIndex]; const stepFile = (delta: number) => { @@ -178,13 +165,9 @@ export function JobDiffs({ job }: JobDiffsProps) { } return ( - /* This row fills the height the page hands it, so the tree is full height by - construction. A fixed viewport calc can't work here: the row starts below the - header and tabs, so `100svh - ` overshot the bottom and forced the - whole page to scroll even for three files. */ + // Fixed viewport math doesn't work here: the row starts below the header and tabs, so + // `100svh - ` overshot and forced the whole page to scroll even for three files.

- {/* File tree - full height, scrolling independently so it stays reachable - in huge PRs. */} - {/* Diff column - scrolls itself rather than growing the page, which is what - keeps the tree beside it full height. */} + {/* Scrolls itself rather than growing the page, which is what keeps the tree beside it full height. */}
- {/* Large-PR banner */} {isLargePr && (
@@ -247,7 +227,6 @@ export function JobDiffs({ job }: JobDiffsProps) {
)} - {/* Summary strip */}
diff --git a/src/client/components/features/job-detail/job-findings-list.tsx b/src/client/components/features/job-detail/job-findings-list.tsx index 4efffa52..c139c065 100644 --- a/src/client/components/features/job-detail/job-findings-list.tsx +++ b/src/client/components/features/job-detail/job-findings-list.tsx @@ -11,7 +11,6 @@ interface JobFindingsListProps { job: JobDetail; } -/** Group panel header: icon + name on the left, mono count on the right. */ function GroupHeader({ children, count, @@ -37,23 +36,18 @@ function GroupHeader({ export function JobFindingsList({ job }: JobFindingsListProps) { const [viewBy, setViewBy] = useState<'files' | 'severity'>('files'); - // Only surface files that actually have something to report - findings or a - // failed review. Clean files are omitted. + // Only surface files with something to report - findings or a failed review. const filesWithIssues = job.files.filter( (f) => f.parsedComments.length > 0 || f.fileStatus === 'failed', ); const failedFiles = job.files.filter((f) => f.fileStatus === 'failed'); - // The badge counts FINDINGS, not files - it sat next to a "Findings" label while - // showing `filesWithIssues.length`, so a job with 9 findings spread over 7 files - // read "Findings 7" and disagreed with the priority triage totals right above it. + // Counts FINDINGS, not files, so it agrees with the priority triage totals above it. const findingCount = job.files.reduce((total, file) => total + file.parsedComments.length, 0); - // This list shows everything the model produced. Most of it never reaches the pull request -- - // the confidence/severity gates, cross-run dedupe and the verification pass all run afterwards. - // Showing 11 findings for a review that posted 1 reads as "Codra reported 11 things", so state - // the posted count explicitly whenever the two differ. + // State the posted count explicitly whenever it differs, since most findings never reach the PR + // (severity/confidence gates, dedupe, verification). const postedCount = job.files.reduce( (total, file) => total + file.parsedComments.filter((c) => c.posted).length, 0, @@ -62,7 +56,6 @@ export function JobFindingsList({ job }: JobFindingsListProps) { return (
- {/* Section header */}
@@ -120,7 +113,6 @@ export function JobFindingsList({ job }: JobFindingsListProps) { > Failed files - {/* Failed files list */}
{failedFiles.map((file) => ( @@ -154,7 +146,6 @@ export function JobFindingsList({ job }: JobFindingsListProps) { > {groupName} - {/* Comment list */}
{comments.map((comment, index) => ( - {/* The header is the detail page's version of a table row: one prominent - title, then a single line of muted status / verdict / metadata chips - - the same vocabulary the jobs table uses. */} + {/* The header is the detail page's version of a table row: same vocabulary as the jobs table. */}
- {/* Breadcrumb - deliberately thin: the repo and PR live in the chip row - below, so this only carries the way back and the job id. */} + {/* Deliberately thin: the repo and PR live in the chip row below, so this only carries the way back and the job id. */}
Jobs @@ -101,7 +98,6 @@ export function JobHeader({ - {/* Identity line: status dot + verdict pill, then the metadata chips. */}
@@ -121,8 +117,7 @@ export function JobHeader({ )} - {/* Branch pair is the widest and least essential chip, so - like the - table's secondary columns - it is capped and drops off first. */} + {/* Branch pair is the widest and least essential chip, so it is capped and drops off first. */} {job.baseRef && job.headRef && ( : } - {/* A single re-run control. It always restarts the review from the beginning (a fresh - review of every file) and works whether the job is finished, failed, or still running. */} + {/* Always restarts the review from the beginning (every file), regardless of the job's current status. */}
- {/* Baseline route card */}
Baseline route @@ -289,7 +286,6 @@ export function ModelRouteEditor({
- {/* Size tier cards */} {tiers.length > 0 && (
{tiers.map((tier, index) => ( diff --git a/src/client/components/features/models/model-route.ts b/src/client/components/features/models/model-route.ts index 073ba573..1bdd2c9f 100644 --- a/src/client/components/features/models/model-route.ts +++ b/src/client/components/features/models/model-route.ts @@ -1,8 +1,5 @@ -// The model-route value type and its pure helpers: normalize, compare, describe. -// -// Separate from model-chain.tsx so repos.tsx and settings.tsx can read and compare routes -// without pulling in the editor UI. model-chain.tsx re-exports all of it, because those pages -// have always imported these names from there. +// Separate from model-chain.tsx so repos.tsx/settings.tsx can read and compare routes without +// pulling in the editor UI; model-chain.tsx re-exports it all for backward-compat imports. export type ProviderOption = { value: string; @@ -35,16 +32,9 @@ export const EMPTY_MODEL_ROUTE: ModelRouteConfig = { size_overrides: [], }; -/** - * Route normalization and comparison, owned here because this module owns `ModelRouteConfig`. - * `repos.tsx` and `settings.tsx` each had copies that drifted in opposite directions; these keep the - * safer half of each. - */ -/** - * Deliberately wider than `Partial`: the stored config types every field as - * nullable and the API returns it raw, so the input type must admit nulls rather than force a cast - * at each call site. - */ +// `repos.tsx` and `settings.tsx` each had their own normalize/compare copies that drifted apart; this +// keeps the safer half of each. Wider than `Partial` since the API returns raw, +// all-nullable fields. export type ModelRouteInput = | { [K in keyof ModelRouteConfig]?: ModelRouteConfig[K] | null } | null diff --git a/src/client/components/features/repos/repo-model-modal.tsx b/src/client/components/features/repos/repo-model-modal.tsx index 118db0eb..9850acb2 100644 --- a/src/client/components/features/repos/repo-model-modal.tsx +++ b/src/client/components/features/repos/repo-model-modal.tsx @@ -16,8 +16,6 @@ import { } from '@client/components/features/models/model-chain'; import { getGlobalRoute, getRepoRoute, hasStoredModelStrategy, repoId, type GlobalModelConfig } from './repo-route'; -// The per-repository model-strategy dialog, including the inherit-from-global toggle. - export interface RepoModelModalProps { repo: RepoConfigRecord | null; globalConfig: GlobalModelConfig | ModelRouteConfig | null; @@ -56,10 +54,8 @@ export function RepoModelModal({ setInitialRoute(nextRoute); setSaving(null); setError(null); - // Keyed on VALUE identity, not object identity: `selectedRepoId` and `globalRouteKey` are a - // string id and a JSON serialization of exactly the inputs `getRepoRoute` reads. Depending on - // `repo`/`globalConfig` directly would reset the user's unsaved edits every time the poll - // returned a structurally identical object. + // Keyed on value identity (id + JSON of the route), not object identity, so a poll returning a + // structurally identical object doesn't reset the user's unsaved edits. // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedRepoId, globalRouteKey]); diff --git a/src/client/components/features/repos/repo-route.ts b/src/client/components/features/repos/repo-route.ts index 021e71eb..e0a1549c 100644 --- a/src/client/components/features/repos/repo-route.ts +++ b/src/client/components/features/repos/repo-route.ts @@ -1,9 +1,7 @@ import { formatDateTime } from '@client/lib/timezone'; import type { RepoConfig, RepoConfigRecord } from '@shared/schema'; import { EMPTY_MODEL_ROUTE, normalizeModelRoute, routesEqual, type ModelRouteConfig } from '@client/components/features/models/model-chain'; -// How a repository resolves its model strategy: inherit the global route, or use a stored -// per-repo one. Shared by the repos page, its rows and the strategy dialog, so it cannot live in -// any of them. +// Shared by the repos page, its rows and the strategy dialog, so it can't live in any single one. export type GlobalModelConfig = RepoConfig['model']; diff --git a/src/client/components/features/repos/repo-row.tsx b/src/client/components/features/repos/repo-row.tsx index 18c01c99..89611a81 100644 --- a/src/client/components/features/repos/repo-row.tsx +++ b/src/client/components/features/repos/repo-row.tsx @@ -6,8 +6,6 @@ import type { RepoConfigRecord } from '@shared/schema'; import { describeModelRoute, type ModelOption, type ModelRouteConfig } from '@client/components/features/models/model-chain'; import { getRepoRoute, hasMeaningfulCustomStrategy, formatLastActivity, type GlobalModelConfig } from './repo-route'; -// One repository row in the repos list. - export interface RepoRowProps { repo: RepoConfigRecord; globalConfig: GlobalModelConfig | ModelRouteConfig | null; @@ -29,8 +27,7 @@ export function RepoRow({ const custom = hasMeaningfulCustomStrategy(repo, globalConfig); const lastActivity = formatLastActivity(repo.lastJobCreatedAt); - // Flat list row (Cloudflare-dashboard style): mono repo name, tint badges, - // plain switch + edit on the right, no per-row card chrome. + // Deliberately no per-row card chrome (Cloudflare-dashboard style flat list). return (
diff --git a/src/client/components/features/settings/about-section.tsx b/src/client/components/features/settings/about-section.tsx index 15d5ebb3..c752cbec 100644 --- a/src/client/components/features/settings/about-section.tsx +++ b/src/client/components/features/settings/about-section.tsx @@ -5,19 +5,15 @@ import { LayerCard } from '@client/components/ui/layer-card'; import { Text } from '@client/components/ui/text'; import { Badge } from '@client/components/ui/badge'; -// Static: version, license and links, all read from package.json at build time. No props and no -// state, which is why it is a component rather than inlined JSX -- it keeps 50 lines of markup out -// of SettingsPage without threading anything through. +// No props and no state, which is why this is a component rather than inlined JSX: it keeps 50 lines of markup out of SettingsPage. export function AboutSection() { return ( - {/* LayerCards: Version/License in one, the links grid in its own. */}
- {/* Version + License */}
Version @@ -34,7 +30,6 @@ export function AboutSection() {
- {/* Links - original 3-column grid, in its own card */}
{[ diff --git a/src/client/components/features/settings/provider-row.tsx b/src/client/components/features/settings/provider-row.tsx index 77611354..f8481e8d 100644 --- a/src/client/components/features/settings/provider-row.tsx +++ b/src/client/components/features/settings/provider-row.tsx @@ -20,10 +20,8 @@ import { type ProviderDraft, } from './settings-support'; -// One row of the LLM Providers list, with its collapsible config panel. Extracted from settings.tsx -// as a component rather than left inline because it is the page's largest single block of markup; -// the section shell around it stays in settings.tsx, so only these eight values are threaded through -// instead of the ~23 the whole section would need. +// Extracted from settings.tsx as its largest single block of markup; the section shell stays put, +// so only these values are threaded through instead of the whole page's state. export function ProviderRow({ provider, savedProviders, @@ -47,8 +45,7 @@ export function ProviderRow({ removeProvider: (id: string) => void | Promise; clearProviderKey: (provider: ProviderDraft) => void | Promise; saving: string | null; - // Passed in rather than imported so this component stays free of the toast singleton, which keeps - // it renderable in isolation. + // Passed in rather than imported so this component stays free of the toast singleton. toast: { error: (message: string, opts?: { description?: string }) => void }; }) { const nativeCloudflare = provider.apiFormat === 'cloudflare-workers-ai'; @@ -69,7 +66,6 @@ export function ProviderRow({ dirty && 'bg-primary/[0.018]', )} > - {/* Row - the whole left side toggles the config panel */}
- {/* Controls */}
- {/* Save - only visible when dirty */} {dirty && (
- {/* Expanded edit panel */} {configOpen && (
@@ -200,10 +193,7 @@ export function ProviderRow({ value={provider.apiKey} onChange={e => { const apiKey = e.target.value; - // Losing the only credential must also drop `enabled`, - // otherwise the switch (which renders enabled && has - // credential) desyncs from the draft and Save would be - // rejected by the server. + // Losing the only credential must also drop `enabled`, or the switch desyncs from the draft and Save is rejected by the server. const losesCredential = !apiKey.trim() && !provider.hasApiKey; updateProviderDraft(provider.id, { apiKey, diff --git a/src/client/components/features/settings/review-section.tsx b/src/client/components/features/settings/review-section.tsx index 77191952..e7bf2a5a 100644 --- a/src/client/components/features/settings/review-section.tsx +++ b/src/client/components/features/settings/review-section.tsx @@ -15,8 +15,7 @@ import { MAX_COMMENTS_STEPS, } from './settings-support'; -// The "Review performance" card plus the confirm dialog that guards its two ceilings. Everything it -// needs comes from useReviewSettings, so the page spreads that hook's return value straight in. +// Everything this needs comes from useReviewSettings, so the page spreads that hook's return value straight in. export function ReviewSection({ loading, reviewSettings, @@ -96,8 +95,7 @@ export function ReviewSection({ step={1} value={maxFilesDraft} onChange={(event) => setMaxFilesDraft(event.target.value)} - // Committed on blur/Enter rather than on every keystroke: this is a free text - // field, and saving mid-typing would persist "2" on the way to "200". + // Committed on blur/Enter, not every keystroke, so typing "200" doesn't persist "2" mid-way. onBlur={commitMaxFiles} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur(); diff --git a/src/client/components/features/settings/settings-support.tsx b/src/client/components/features/settings/settings-support.tsx index fef21820..bdeb9dd8 100644 --- a/src/client/components/features/settings/settings-support.tsx +++ b/src/client/components/features/settings/settings-support.tsx @@ -1,9 +1,7 @@ import type { LlmApiFormat, LlmProvider } from '@shared/schema'; import { REVIEW_CONCURRENCY_LIMITS, reviewMaxCommentsOptions, type ReviewConcurrencyLevel } from '@shared/review-limits'; -// Module-scope constants, provider-draft predicates and the shared field label for the settings -// page. Pure and render-free apart from FieldLabel, so the page and its section components can all -// depend on it without either depending on the other. +// Pure and render-free apart from FieldLabel, so the settings page and its sections can all depend on it without depending on each other. export const API_FORMAT_OPTIONS: Array<{ value: LlmApiFormat; label: string }> = [ { value: 'openai', label: 'OpenAI' }, @@ -98,7 +96,6 @@ export function providerDraftDirty(provider: ProviderDraft, saved?: LlmProvider) ); } -/* โ”€โ”€โ”€ Field label โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ export function FieldLabel({ htmlFor, id, children }: { htmlFor: string; id?: string; children: React.ReactNode }) { return (