diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8c936d7a..7580e52c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,13 +31,13 @@ What actually happened. Include error messages verbatim. ## Environment -> From **5.5.0** the minimum supported IDE is **2025.3 (build 253)**. The whole chat UI is the IDE's -> embedded browser, and the module that provides it does not exist before 253. On 2025.1 or 2025.2 the -> last supported version is **5.1.1** — a bug report against 5.5.0 on those builds is expected behaviour, -> not a defect. +> From **6.5.0** the minimum supported IDE is **2026.2 (build 262.8665.258)**. On 2025.3 or 2026.1 the +> last supported version is **6.0.1**; on 2025.1 or 2025.2 it is **5.1.1**. A bug report against 6.5.0 on +> an older build is expected behaviour, not a defect. In Remote Development, say whether the plugin is +> installed on the host, the client, or both. - **OS:** (e.g. Ubuntu 24.04, macOS 14.5, Windows 11 23H2) -- **IDE:** (Help → About → product + build, e.g. `IntelliJ IDEA 2025.3 IC-253.28294.334`) +- **IDE:** (Help → About → product + build, e.g. `IntelliJ IDEA 2026.2.3 IU-262.10968.63`) - **Plugin version:** (Settings → Plugins → Claude Code Native) - **`claude` binary version:** output of `claude --version` - **Binary location:** `which claude` (Linux/macOS) or `where claude` (Windows) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a2a15be0..3db9256c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,9 +9,9 @@ contact_links: # TODO: replace with the GitHub Discussions URL once enabled for the repo. - name: GitHub Discussions - url: https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/discussions + url: https://github.com/serialexperimentslainnnn/claude-code-native/discussions about: Ask a question, share a workflow, or discuss ideas before filing an issue. - name: Security vulnerability - url: https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/security/advisories/new + url: https://github.com/serialexperimentslainnnn/claude-code-native/security/advisories/new about: Do NOT open a public issue. Report it privately here; see SECURITY.md. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e73cf056..21cdc9a7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,7 +32,7 @@ or the permission surface, say what happens to a user who already ran it. - [ ] Commits follow Conventional Commits (the `commit-msg` hook enforces it — install once with `git config core.hooksPath .githooks`). - [ ] `./gradlew test verifyPlugin buildPlugin` passes locally. -- [ ] `verifyPlugin` is **Compatible** across the declared range (253 → 263.\*) +- [ ] `verifyPlugin` is **Compatible** across the declared range (262.8665.258 → 263.\*) and reports no new internal-API usage (`@ApiStatus.Internal`). The CDN download is unreliable here; use `-PlocalIdePath=[,…]` with locally-extracted IDEs. @@ -45,7 +45,7 @@ or the permission surface, say what happens to a user who already ran it. - [ ] No new deprecated or scheduled-for-removal IntelliJ Platform APIs. - [ ] Tests added or updated for the new behaviour — `src/test/kotlin/…` for Kotlin, `src/test/frontend/…` (`npm test`) for anything under - `src/main/resources/jcef/`. + `frontend/src/main/resources/jcef/` or `frontend/src/main/ts/`. - [ ] Protocol changes: `./gradlew checkDrift` is green and the baseline in `scripts/drift-baseline.properties` matches what was verified. - [ ] New dependency? Its licence is compatible with GPL-3.0-only and it is diff --git a/.github/ci-image/jvm-test.Dockerfile b/.github/ci-image/jvm-test.Dockerfile index df98eb9a..06c20537 100644 --- a/.github/ci-image/jvm-test.Dockerfile +++ b/.github/ci-image/jvm-test.Dockerfile @@ -43,7 +43,7 @@ # Declared before FROM so it can be used there. The default names this repository's own package; a fork # overrides it with --build-arg rather than editing the file. -ARG NODE_IMAGE=ghcr.io/serialexperimentslainnnn/node-test:v1.0.0 +ARG NODE_IMAGE=ghcr.io/serialexperimentslainnnn/node-test:v1.1.0 FROM ${NODE_IMAGE} # NB there is no dnf tuning here and that is not an omission: `max_parallel_downloads=20` and @@ -51,14 +51,6 @@ FROM ${NODE_IMAGE} # filesystem — so the JDK transaction below already runs with them. Adding the lines again would append a # SECOND copy of each key to dnf.conf rather than overriding anything. # -# Temurin, not Fedora's OpenJDK. -# -# Fedora 44 no longer packages java-21-openjdk — it has moved on to a newer LTS — and the JDK version is not -# ours to float: build.gradle.kts pins the toolchain to 21 because the IDE runs on JBR 21, which is the -# ceiling. Building on 25 would produce class files no target IDE can load. Adoptium's repository is the -# same source the `setup-java` action uses on the hosted runners, so the image and the pipeline compile -# against the same JDK rather than two different builds of "21". -# # There is deliberately no `dnf-plugins-core`: nothing here calls `dnf config-manager` — the repo file is # written with `printf` — and `curl` is already in the base image, so installing it dragged in a ~150 MB # Python stack to run a command nobody ran. @@ -81,7 +73,7 @@ RUN curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public \ 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Adoptium' \ > /etc/yum.repos.d/adoptium.repo \ && dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \ - temurin-21-jdk \ + temurin-25-jdk \ python3 \ zip \ && dnf clean all \ @@ -125,9 +117,9 @@ RUN dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \ # silently break on the next base-image bump. The symlink keeps the ENV below stable across rebuilds. RUN JH="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" \ && echo "JAVA_HOME=$JH" >> /etc/environment \ - && ln -sfn "$JH" /opt/java-21 \ + && ln -sfn "$JH" /opt/java-25 \ && "$JH/bin/java" -version -ENV JAVA_HOME=/opt/java-21 +ENV JAVA_HOME=/opt/java-25 ENV PATH="${JAVA_HOME}/bin:${PATH}" # Gradle writes here, and the path MUST match GRADLE_USER_HOME in the workflow. If they diverge, the warm diff --git a/.github/ci-image/node-test.Dockerfile b/.github/ci-image/node-test.Dockerfile index 2e42aa54..4a59d44f 100644 --- a/.github/ci-image/node-test.Dockerfile +++ b/.github/ci-image/node-test.Dockerfile @@ -19,8 +19,8 @@ # BUILDING — from the repository ROOT, so /.dockerignore applies: # # docker build -f .github/ci-image/node-test.Dockerfile \ -# -t ghcr.io/OWNER/node-test:v1.0.0 . -# docker push ghcr.io/OWNER/node-test:v1.0.0 +# -t ghcr.io/OWNER/node-test:v1.1.0 . +# docker push ghcr.io/OWNER/node-test:v1.1.0 # # The tag is `vMAJOR.MINOR.PATCH`, never `latest`: a floating tag makes "which image was that job green on?" # unanswerable, and this repository's standard is to pin. Bumping it is a commit — change the tag here and @@ -49,7 +49,7 @@ RUN echo "max_parallel_downloads=20" >> /etc/dnf/dnf.conf \ # not, and bumping the tag is the deliberate act that moves it. RUN dnf -y upgrade --refresh \ && dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \ - nodejs npm \ + nodejs24 nodejs24-bin nodejs24-npm nodejs24-npm-bin \ git-core unzip tar which findutils procps-ng ca-certificates \ && dnf clean all \ && rm -rf /var/cache/dnf \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19edeebd..e4de57aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,14 +88,14 @@ jobs: # EVERY job in this file runs in this image, and the image is the ONLY caching mechanism. # # `gradle/actions/setup-gradle` used to sit in the heavy jobs and was quietly useless here: the warm - # GRADLE_USER_HOME measures 31 GB (23 GB of extracted IDE transforms under caches/9.5.1, 7.7 GB of the + # GRADLE_USER_HOME measures 31 GB (23 GB of extracted IDE transforms under caches/9.7.1, 7.7 GB of the # downloaded IDE artifacts under modules-2), and a GitHub Actions cache entry is capped at 10 GB per # repository. It could never have stored what it appeared to be storing — it was saving a partial # cache, evicting it, and re-downloading the rest on the next run. The image has no such ceiling, and # the trade is explicit: refreshing what CI has cached now means rebuilding and pushing the image, # which is a deliberate act rather than something that drifts between runs. container: - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. This requires the package to have been granted Read access to # THIS repository (package settings -> Manage Actions access): `packages: read` widens what the token @@ -158,7 +158,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 container: - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. `packages: read` is granted per job below; without it the pull # fails with a 401 that reads like a wrong image name rather than a permission problem. @@ -229,7 +229,7 @@ jobs: build/reports/kover/ retention-days: 14 - # The JCEF web app (src/main/resources/jcef/*.js) under vitest + jsdom. Nothing here ships in the + # The JCEF web app (frontend/src/main/resources/jcef/*.js) under vitest + jsdom. Nothing here ships in the # plugin — vitest and jsdom are devDependencies — but the code under test absolutely does. frontend-test: name: Frontend tests @@ -239,7 +239,7 @@ jobs: # `node-test`, not `jvm-test`: this job is `npm ci` and then vitest. On the single combined image it # pulled a JDK, a Gradle distribution and 3.4 GB of extracted IntelliJ Platform it never opened — # 1m05s of container init for 8 seconds of work. - image: ghcr.io/serialexperimentslainnnn/node-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/node-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. `packages: read` is granted per job below; without it the pull # fails with a 401 that reads like a wrong image name rather than a permission problem. @@ -283,7 +283,7 @@ jobs: timeout-minutes: 10 container: # `node-test`: this job is `npm ci` and two `npm audit` invocations. Nothing here touches the JVM. - image: ghcr.io/serialexperimentslainnnn/node-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/node-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. `packages: read` is granted per job below; without it the pull # fails with a 401 that reads like a wrong image name rather than a permission problem. @@ -389,7 +389,7 @@ jobs: # Same image as every other job, and it does NOT carry the IDEs this job downloads — see the note at # the top of .github/ci-image/jvm-test.Dockerfile. Baking them made the image 38.1 GB, which every job paid for # on its own runner, to save ten minutes on the one job that runs least often. - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. `packages: read` is granted per job below; without it the pull # fails with a 401 that reads like a wrong image name rather than a permission problem. @@ -522,13 +522,22 @@ jobs: run: | zip=$(ls build/distributions/*.zip) unzip -o -q "$zip" -d /tmp/artifact - jar=$(ls /tmp/artifact/*/lib/claude-code-native-*.jar | grep -v searchableOptions) - # -1 lists entry names alone; directory entries end in `/` and carry nothing. - unzip -Z -1 "$jar" | grep -v '/$' \ - | grep -vE '^dev/lain/claudejb/.*\.class$' \ - | grep -vE '^META-INF/' \ - | grep -vE '^icons/[^/]+\.svg$' \ - | grep -vE '^jcef/(.+\.(js|html)|css/.+\.css)$' > /tmp/unexpected.txt || true + ls /tmp/artifact/*/lib/dev.lain.claudejb-*.jar /tmp/artifact/*/lib/modules/dev.lain.claudejb.*.jar 2>/dev/null \ + | grep -v searchableOptions > /tmp/jars.txt || true + if [ "$(wc -l < /tmp/jars.txt)" -lt 4 ]; then + echo "::error::expected the plugin jar and the shared, frontend and backend module jars" + find /tmp/artifact -name '*.jar' + exit 1 + fi + : > /tmp/unexpected.txt + while IFS= read -r jar; do + unzip -Z -1 "$jar" | grep -v '/$' \ + | grep -vE '^dev/lain/claudejb/.*\.class$' \ + | grep -vE '^META-INF/' \ + | grep -vE '^dev\.lain\.claudejb\.(shared|frontend|backend|git|github|java|intellilang|terminal|bookmarks|database|problems)\.xml$' \ + | grep -vE '^icons/[^/]+\.svg$' \ + | grep -vE '^jcef/(.+\.(js|html)|css/.+\.css)$' >> /tmp/unexpected.txt || true + done < /tmp/jars.txt if [ -s /tmp/unexpected.txt ]; then echo "::error::unexpected entries in the plugin jar — either they must not ship, or add their family to this allowlist" cat /tmp/unexpected.txt @@ -556,7 +565,7 @@ jobs: zip=$(ls build/distributions/*.zip) unzip -o -q "$zip" -d /tmp/dist - jar=$(ls /tmp/dist/*/lib/claude-code-native-*.jar | grep -v searchableOptions | head -1) + jar=$(ls /tmp/dist/*/lib/dev.lain.claudejb-*.jar | grep -v searchableOptions | head -1) # ONE listing, read by every assertion below. `-Z1` prints one entry name per line, so names are # matched WHOLE (`grep -qxF`) instead of as substrings of `unzip -l`'s formatted table — where @@ -627,7 +636,7 @@ jobs: # dead IDE into a fast, explained failure instead of letting it eat the whole budget. timeout-minutes: 45 container: - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 # The package stays PRIVATE and is pulled with the run's own GITHUB_TOKEN — no new secret, nothing to # rotate, and access dies with the job. `packages: read` is granted per job below; without it the pull # fails with a 401 that reads like a wrong image name rather than a permission problem. @@ -652,32 +661,6 @@ jobs: with: persist-credentials: false - # The image running this job carries no X11 whatsoever: it was built for headless Gradle and npm, and - # nothing in it has ever had to draw. This is the one job that does — a real IDE, with a real Chromium - # inside it. - # - # .github/ci-image/jvm-test.Dockerfile ALREADY bakes this exact package set, which is where it belongs: - # the image is this pipeline's only caching mechanism, so a `dnf install` per run is a network - # transaction whose failure mode is this job's worst one. This step survives because every workflow - # pins `jvm-test:v1.0.0`, and that tag names an image built BEFORE the Dockerfile gained the stack. A - # Dockerfile edit changes nothing on its own; the image has to be rebuilt and a new tag cut. - # - # DELETE THIS STEP in the same change that bumps the tag — in every workflow that names it, not just - # this one (ci.yml ×4, release.yml, codeql.yml, drift.yml). Leaving it behind costs one redundant - # install per nightly run; deleting it BEFORE the tag moves costs an IDE that never opens its port. - # - # A missing library here does not announce itself — the IDE simply never opens :8082 — which is exactly - # the silence the bounded wait below turns back into a message. - - name: Install the virtual display and the libraries the IDE draws through - run: | - set -euo pipefail - dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \ - xorg-x11-server-Xvfb \ - gtk3 nss alsa-lib mesa-libgbm libxkbcommon-x11 \ - libXtst libXi libXrender libXext libXrandr libXcursor \ - liberation-fonts - dnf clean all - # Step one of the two-process dance that build.gradle.kts and docs/UI_TESTING.md both document: the IDE # comes up under Xvfb and STAYS UP, and the suite is a second Gradle invocation that talks to it over # :8082. Backgrounded here rather than split into a second job, because the two halves must share a host. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b3a27753..3b527dc5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: timeout-minutes: 45 container: # `jvm-test`: the manual build is `./gradlew classes`, which resolves the whole IntelliJ Platform. - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -58,11 +58,11 @@ jobs: with: persist-credentials: false - # No `setup-java` step: the image already carries the Temurin 21 the rest of the pipeline builds + # No `setup-java` step: the image already carries the Temurin 25 the rest of the pipeline builds # with. Provisioning a second JDK here meant CodeQL analysed a build that used a different one. - name: Initialize CodeQL - uses: github/codeql-action/init@18420e3271f74589575af831a523c833acda327f # codeql-bundle-v2.26.2 + uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: languages: java-kotlin build-mode: manual @@ -160,7 +160,7 @@ jobs: run: ./gradlew --no-daemon --stacktrace classes - name: Analyze - uses: github/codeql-action/analyze@18420e3271f74589575af831a523c833acda327f # codeql-bundle-v2.26.2 + uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: category: /language:java-kotlin @@ -177,13 +177,13 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@18420e3271f74589575af831a523c833acda327f # codeql-bundle-v2.26.2 + uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: languages: javascript-typescript build-mode: none queries: security-extended - name: Analyze - uses: github/codeql-action/analyze@18420e3271f74589575af831a523c833acda327f # codeql-bundle-v2.26.2 + uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1 with: category: /language:javascript-typescript diff --git a/.github/workflows/drift.yml b/.github/workflows/drift.yml index e1d8c372..783e2c96 100644 --- a/.github/workflows/drift.yml +++ b/.github/workflows/drift.yml @@ -34,7 +34,7 @@ jobs: timeout-minutes: 30 container: # `jvm-test`: this job runs `npm install` and the global claude CLI install AND `./gradlew checkDrift`. - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21214e5d..cf7cd27a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,7 +148,7 @@ jobs: # gate could pass or fail on a toolchain the pull request never saw. container: # `jvm-test`: this gate runs `npm ci`, `npm test` AND `./gradlew test verifyPlugin` in one job. - image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.0.0 + image: ghcr.io/serialexperimentslainnnn/jvm-test:v1.1.0 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -396,10 +396,10 @@ jobs: with: node-version-file: .nvmrc - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: temurin - java-version: '21' + java-version: '25' # NB no `setup-gradle`, deliberately, and this was the last one left in any workflow. # @@ -450,7 +450,7 @@ jobs: echo "published $name sha256=$(sha256sum "dist/$name" | cut -d' ' -f1)" - name: Attest build provenance - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: dist/${{ steps.artifact.outputs.name }} diff --git a/.gitignore b/.gitignore index 43df8a93..2c414cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,17 @@ # Source and resources # ------------------------------------------------------------------------------------------ !/src/** +!/shared/** +!/frontend/** +!/backend/** +!/git/** +!/github/** +!/java/** +!/intellilang/** +!/bookmarks/** +!/database/** +!/problems/** +!/terminal/** !/bin/** !/gradle/** !/config/** @@ -140,3 +151,4 @@ PROJECTMAP.md # under .claude/worktrees/, and an unanchored pattern would also hide a real directory of that name # somewhere in the tree. Committing it would commit a copy of the repository into the repository. /.claude/ +/.claudetools/ diff --git a/.nvmrc b/.nvmrc index 2bd5a0a9..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +24 diff --git a/.prettierignore b/.prettierignore index fd5a03d1..7ee234be 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ # Vendored third-party bundles — redistributed unmodified; reformatting them would fork a dependency. -src/main/resources/jcef/marked.min.js -src/main/resources/jcef/purify.min.js -src/main/resources/jcef/highlight.min.js +frontend/src/main/resources/jcef/marked.min.js +frontend/src/main/resources/jcef/purify.min.js +frontend/src/main/resources/jcef/highlight.min.js build/ node_modules/ diff --git a/AGENTS.md b/AGENTS.md index 4af10045..8a9fdcea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,8 @@ Division of labour, so neither file rots: | Requirement | Value | Note | |---|---|---| -| JDK | **21**, the JetBrains Runtime | `export JAVA_HOME=~/.jdks/jbr-21.0.11` (or your JBR 21). The IDE runs on JBR 21 — that is the ceiling, not a preference. | -| Gradle | wrapper, **9.5.1** | Always `./gradlew`, never a system Gradle. | +| JDK | **25**, the JetBrains Runtime | `JAVA_HOME` pointing at your JBR 25. The 2026.2 floor runs on JBR 25; the toolchain matches it. | +| Gradle | wrapper, **9.7.1** | Always `./gradlew`, never a system Gradle. | | Node | any current LTS | Frontend tests only. Nothing from npm ships in the plugin. | | `claude` binary | preinstalled, on `PATH` or `~/.local/bin` | Required at *runtime* by the plugin and by `checkDrift`. The plugin never downloads one. | @@ -29,11 +29,11 @@ will miss and report as a successful build. ```sh ./gradlew test # unit + headless component + integration. The gate. -npm test # frontend tests (vitest + jsdom) over the real resources/jcef/*.js +npm test # frontend tests (vitest + jsdom) over the real frontend/src/main/resources/jcef ./gradlew detekt spotlessCheck # static analysis + formatting, both gated in CI npm run lint && npm run format:check # the same two, for the shipped JCEF JavaScript ./gradlew buildPlugin # → build/distributions/*.zip -./gradlew verifyPlugin # compatibility across the declared range (253 → 263.*) +./gradlew verifyPlugin # compatibility across the declared range (262.8665.258 → 263.*) ./gradlew runIde # sandbox IDE with the plugin loaded ./gradlew checkDrift # protocol drift vs the live binary + SDK (updates both, then reports) ./gradlew koverHtmlReport # coverage (koverVerify is the gate, and runs with `test` in CI) @@ -42,11 +42,11 @@ npm run lint && npm run format:check # the same two, for the shipped JCEF Java Test counts are deliberately not written here: they change every release and a number in a runbook is a claim nobody re-checks. `./gradlew test` and `npm test` report their own. -**The floor is 253 (2025.3), not 251.** `sinceBuild` moved in 5.5.0 because the whole UI is JCEF and -`com.intellij.modules.jcef` — declared **hard** in `plugin.xml` — does not exist as a module id before 253. -Do not "fix" a verifier complaint by widening it back or by making that dependency optional: an optional -dependency that cannot be satisfied is skipped, which is exactly the silent breakage on 262 this replaced. -`JcefDependencyContractTest` is the gate and it is mutation-checked. +**The floor is 2026.2 (262.8665.258).** The plugin is split into `shared`, `frontend` and `backend` content +modules that talk over the platform RPC, and that RPC is internal API before 2026.x: the floor is where it +became public. `com.intellij.modules.jcef` is declared **hard** by the frontend module. Do not "fix" a +verifier complaint by widening the range or by making that dependency optional: an optional dependency that +cannot be satisfied is skipped silently. `JcefDependencyContractTest` is the gate and it is mutation-checked. `verifyPlugin`'s CDN download is unreliable here. Use locally-extracted IDEs: `./gradlew verifyPlugin -PlocalIdePath=[,…]` (comma-separated). @@ -157,7 +157,7 @@ end up stale, and CLAUDE.md is the one that carries the reasoning. Read it; the that are about *working*, not about the design. - **Frontend changes need frontend tests.** The JS↔CSS class contract test exists because a missing CSS rule - once shipped silently. `src/main/resources/jcef/*.js` is loaded for real by `src/test/frontend/`, so a + once shipped silently. `frontend/src/main/resources/jcef` is loaded for real by `src/test/frontend/`, so a module you add is a module the harness must be told to load. - **UI changes need a keyboard pass.** Automated checks catch roughly half of accessibility barriers and none of the judgement calls. Drive what you changed with the keyboard alone and confirm the focus ring is visible. diff --git a/CHANGELOG.md b/CHANGELOG.md index 40fad6a0..45cf6dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,96 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.5.0] — 2026-10-02 + +**The chat works in Remote Development.** The plugin is split into a frontend, drawn where the UI runs, and a +backend, beside the project, joined by the platform's RPC. **This release needs IntelliJ Platform 2026.2 +(build 262.8665.258) or newer.** On 2025.3.1 or 2026.1 stay on 6.0.1, or update the IDE. + +### Added +- **Remote Development.** The plugin is a split plugin with three content modules: `dev.lain.claudejb.shared` + holds the RPC chat contract (`ChatApi`); `dev.lain.claudejb.frontend` the tool window, the embedded browser, + the page serving and the theme, and loads where the UI runs — JetBrains Client in Remote Development; + `dev.lain.claudejb.backend` the sessions, the MCP servers, the guard, the settings, Git and the diffs, and + loads on the host. The page talks to the backend over the platform's RPC. A local IDE, Remote Development + and the Code With Me host take one code path: no mode detection, no port forwarding. The plugin is installed + on the host and on the client; the client gets its copy from the Marketplace through plugin sync. +- **Code With Me, for the host.** The host's chat works as in a local IDE. Guests get no chat: JetBrains is + retiring Code With Me, and 2026.1 was the last release with official support. +- **Optional modules for the IDE plugins the tools reach** — Git, GitHub, Java, Terminal, IntelliLang, + Database, line bookmarks and, on 2026.3, the Problems view. Each loads only when its plugin is present. +- **The README and the plugin page recommend + [Claude Code Native — Skills and Settings](https://github.com/serialexperimentslainnnn/claude-code-native-skills-and-settings)**, + the `~/.claude` configuration that tells Claude how to use the IDE's tools and keeps it using them. + +### Changed +- **The floor is 2026.2 (build 262.8665.258); the range runs to 263.\*.** 2025.3 and 2026.1 are no longer + supported: the RPC API the split rests on is internal there, and the plugin uses no internal API. +- **Streaming no longer re-renders the whole message every 30 ms.** Assistant deltas arrive on a coalescing + drain and are appended; running rows and live tool output are patched in place. +- **Lighter on the IDE.** The VFS is refreshed only after tools that write, not after every MCP call; live tool + output is coalesced and edits are diffed off the EDT; prompts are written to the CLI off the EDT; the page is + assembled once per IDE as one script; restoring a chat reads its transcript from the end; agent transcripts + are tailed from their last offset; background-task output is kept in a ring; the quota is polled every 3 s + and at each message boundary; the MCP helper JVMs start small. +- **Smaller MCP answers.** `read_file` splits its budget between the files of a batch; `search_text` and + `project_problems` group by file; every `max` a tool takes has a ceiling; commit rows are abbreviated and + working-tree diffs built file by file; the services listing is trimmed. +- **`project_problems` and `problems_view` are offered only where the IDE lets a plugin read the Problems + view.** On 2026.3, where the Problems view is its own plugin, they reach it through that plugin's modules. +- **`plugins` says that parts of the IDE shipped as modules, such as IntelliLang, are not listed**, so an + absent entry is not read as a missing feature. +- **A `run ▸ shell` card shows its command as its own code block**, first and visible while the card is + collapsed, whose Copy copies the command alone; the call's other arguments are a block apart. A restored + session draws it the same way. +- **Libraries.** DOMPurify 3.4.16, highlight.js 11.12.0, marked 18.0.14. Build: Kotlin 2.4, a JDK 25 + toolchain, Gradle 9.7.1, IntelliJ Platform Gradle Plugin 2.19.0, detekt 2.0.0-alpha.6, Spotless 8.10.2, + JUnit 6.1.3. kotlinx-serialization is no longer bundled; the platform's copy is used. The npm toolchain + stays on its locked versions. + +### Fixed +- **The trust dialog no longer reappears every 3 s after *Cancel*,** and **a CLI that crashes on start is no + longer respawned forever.** The launch gates run off the EDT and the boot watcher stops on a refusal or a + crash. +- **Closed chat tabs no longer leak memory.** A closed chat is disposed through the Disposer, and open tabs + are persisted off the EDT. +- **The IDE no longer freezes when `claude` stops reading its input.** A CLI blocked on stdin is ended, and + its stdout is scanned once. +- **Answers stream as they are written.** A streaming entry stays running until its message settles. +- **A prompt with attachments is no longer lost when the session cannot start.** +- **One malformed MCP frame no longer takes an MCP server down**, frame headers are bounded, and the bridge's + reply thread stays alive. +- **`run_configuration` reports the exit code of the run it started.** +- **Batches no longer half-apply.** Every call and every batch item gets an answer whatever the tool throws, + and folded batch results settle clean items as successes. +- **A control request always completes**, even on a malformed reply or a long run. +- **Unified diffs count their context lines and merge overlapping hunks.** +- **Shell processes and finished jobs are released**, and each line of a run's output tail is capped. +- **A prompted Git action is released when no turn starts.** +- **The environment script is sourced with the POSIX shell.** +- **The vulnerability database download is bounded in time and size.** +- **The chat page works from the keyboard and with a screen reader.** Tool cards have a keyboard toggle and + no endless animation; menus, the attach menu and the buttons are reachable and operable; the composer + toggles expose their state with `aria-pressed`; Enter while an IME is composing no longer sends; focus and + current data survive dashboard and tab redraws; a settings row shows as pending until the host confirms it. +- **Closing the last chat tab shows the new one.** +- **`scratch_create` keeps the name it is given.** A scratch with the same name and another extension no + longer makes the IDE number the new one; only an exact match does. The answer carries the language the IDE + resolved, or none, instead of an empty string. + +### Security +- **A renewed credential is kept when the keyring refuses the write**, instead of being wiped. +- **An MCP admission credit is granted only after the spawn succeeds, and it expires.** +- **The token file rotates with an atomic move.** +- **An API key is verified before it is approved**, and refused when it cannot be verified. +- **Guard decisions no longer block the protocol reader**, and the alert list is bounded. +- **A permission card resolves once and is rebuilt when its content changes**; a cancelled permission is + withdrawn in order, behind the decision it cancels. +- **Each guard alert is announced once.** +- **Only http(s), `jb://` and relative links are forwarded from the page**, middle clicks included. +- **The guard's outside-project rule lets the IDE's own scratch folder through, and nothing else there.** + Every other rule still judges a scratch file. + ## [6.0.1] — 2026-09-22 ### Fixed diff --git a/DIRECTIVES.md b/DIRECTIVES.md index 9f6adf07..da806638 100644 --- a/DIRECTIVES.md +++ b/DIRECTIVES.md @@ -35,6 +35,14 @@ that, and the plugin serves them. `@ApiStatus.Internal` is not: it does not even promise to stabilise. The forbidden symbols, each with its replacement, are in [`docs/PLATFORM_API_POLICY.md`](docs/PLATFORM_API_POLICY.md). +## Modules + +**Three content modules, one code path.** `dev.lain.claudejb.shared` holds the RPC chat contract (`ChatApi`); +`dev.lain.claudejb.frontend` holds the tool window, the embedded browser, the page and its theme, and runs where +the UI runs; `dev.lain.claudejb.backend` holds the sessions, the MCP servers, the guard, the settings, Git and +the diffs, and runs beside the project. **The frontend depends only on the platform and the shared contract.** +A local IDE, Remote Development and the Code With Me host take the same path; nothing asks which mode it is in. + ## Token cost What gets added has to **do more while spending less**. It is an acceptance criterion, not an @@ -60,8 +68,10 @@ The order of work and what is done lives in [`docs/MCP_ROADMAP.md`](docs/MCP_ROA 6. **Agent-agnostic**: any MCP client can connect. 7. **Nothing blocks waiting**: a queue per server, immediate acknowledgement, out-of-order replies correlated by `id`, a timeout and cancellation on every tool, and a bounded queue depth. -8. **Coroutines only in the new MCP code** (`model/mcp/`, `controller/mcp/`). The rest of the plugin - keeps `AppExecutorUtil`, `ReadAction.compute`, `WriteCommandAction` and the single `edt {}`. +8. **Coroutines only in the MCP code** (`model/mcp/`, `controller/mcp/`) **and in the RPC packages** + (`dev.lain.claudejb.rpc`, `dev.lain.claudejb.rpc.backend`, `dev.lain.claudejb.frontend.rpc`), because the + platform RPC is `suspend` and `Flow`. The rest of the plugin keeps `AppExecutorUtil`, `ReadAction.compute`, + `WriteCommandAction` and the single `edt {}`. ### Server authentication diff --git a/README.md b/README.md index ca979abc..9bc4f0e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Claude Code Native -[![Version](https://img.shields.io/badge/version-6.0.0-E07B5A)](CHANGELOG.md) -[![IDE](https://img.shields.io/badge/JetBrains-2025.3.1%20%E2%86%92%20263.*-000000?logo=jetbrains)](#requirements) +[![Version](https://img.shields.io/badge/version-6.5.0-E07B5A)](CHANGELOG.md) +[![IDE](https://img.shields.io/badge/JetBrains-2026.2%20%E2%86%92%20263.*-000000?logo=jetbrains)](#requirements) [![Marketplace](https://img.shields.io/badge/Marketplace-Claude%20Code%20Native-2A2A2A)](https://plugins.jetbrains.com/plugin/31965-claude-code-native) [![License](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE) @@ -19,6 +19,7 @@ JetBrains. It needs your own `claude` CLI and your own Claude subscription or AP ## Contents - [Requirements](#requirements) · [Installation](#installation) · [First run](#first-run) +- [Recommended: skills and settings](#recommended-skills-and-settings) - [What you can ask for](#what-you-can-ask-for) — the manual - [Open it, show me, take me there](#open-it-show-me-take-me-there) - [Ask about what is on screen](#ask-about-what-is-on-screen) @@ -39,11 +40,15 @@ JetBrains. It needs your own `claude` CLI and your own Claude subscription or AP ## Requirements -**A JetBrains IDE on 2025.3.1 or newer** (`sinceBuild 253.29346.138`, `untilBuild 263.*`): IntelliJ IDEA, +**A JetBrains IDE on 2026.2 or newer** (`sinceBuild 262.8665.258`, `untilBuild 263.*`): IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, GoLand, RubyMine, CLion, Rider, DataGrip, DataSpell, Aqua, RustRover. The floor -is hard: the chat is the IDE's embedded browser (JCEF), which from build 262 is a bundled plugin the plugin -must declare, and that module id first exists in 2025.3.1. On 2025.1, 2025.2 or 2025.3.0 stay on plugin -5.1.1, or update the IDE. +is hard: the chat is split between the IDE's frontend and its backend so that it works in Remote Development, +and the platform RPC that joins them is internal before 2026.2. On 2025.3.1 or 2026.1 stay on plugin 6.0.1, +on 2025.1, 2025.2 or 2025.3.0 on 5.1.1, or update the IDE. + +**Remote Development works.** The chat is drawn in JetBrains Client and the sessions run on the host, beside +the project, over the platform's RPC: no port to forward. Install the plugin on the host; the client gets its +copy from the Marketplace through plugin sync. In **Code With Me** only the host has the chat; guests get none. **The `claude` CLI.** You do not have to install it yourself: if the plugin cannot find it, its first screen offers the official install route for your OS and runs it in the IDE terminal. It looks first at **Settings @@ -62,7 +67,7 @@ Kubernetes, SSH, Deployment, Qodana, Package Checker. Everything else needs noth 1. **Settings ▸ Plugins ▸ Marketplace**, search **Claude Code Native**, install, restart. 2. Or install a signed archive from the - [GitHub releases](https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/releases) with + [GitHub releases](https://github.com/serialexperimentslainnnn/claude-code-native/releases) with **Settings ▸ Plugins ▸ ⚙ ▸ Install Plugin from Disk**. The **Claude Code** tool window appears on the right. @@ -97,6 +102,29 @@ transcript. Settings live in the same safe, one document per IDE installation pe The IDE integration is **on by default** — the flame in the chat bar is lit: all four servers, every rule. There is nothing to configure before you start asking. +## Recommended: skills and settings + +The plugin gives Claude the IDE's tools; **[Claude Code Native — Skills and +Settings](https://github.com/serialexperimentslainnnn/claude-code-native-skills-and-settings)** tells Claude +how to use them, and keeps it using them as a conversation grows. It installs onto `~/.claude`: + +- a working method injected on **every prompt** by a `UserPromptSubmit` hook — the IDE's servers are the + tools, every call batched, everything written first and built and tested once, plain commands, every answer + read; +- `ide-tools-standards`, the skill that names which IDE tool does each job and how the guard answers; +- a catalogue of engineering-standards skills, loaded only when a task touches their domain, and + orchestration workflows installed as slash commands. + +```bash +git clone https://github.com/serialexperimentslainnnn/claude-code-native-skills-and-settings.git +cd claude-code-native-skills-and-settings +./install.sh --dry-run +./install.sh +``` + +On Windows, `pwsh -File .\install.ps1 -WhatIf`, then `pwsh -File .\install.ps1`. Both installers back up +what they replace and take `--uninstall` / `-Uninstall`. + ## What you can ask for This is the manual. Every chapter is a kind of request in your own words, what Claude does with it and what @@ -231,7 +259,8 @@ shows the execution point as it goes; the gutter shows the breakpoints. A command runs in the IDE's Terminal window, in a tab named **Claude**, and comes back with its exit code and output; several commands go in one call. The tab is shown but never focused and never switched while you are in the Terminal, and the output stays there when the tab is reused, so *View in terminal* on the -card lands on it. Claude's own `Bash` is retired while the IDE serves: a new process would cost a guard +card lands on it. The card shows the command as its own code block, whose Copy copies the command alone, and +the call's other arguments in a block apart. Claude's own `Bash` is retired while the IDE serves: a new process would cost a guard pass and a permission, and the IDE already has a shell. ### Git @@ -442,17 +471,16 @@ exports and imports a settings file and migrates from another JetBrains IDE on t The four servers are ordinary MCP servers that happen to live inside the plugin. Claude Code is their first client, not their only one: anything that speaks MCP can open the socket, authenticate with the session's token and run the same tools under the same guard. The bundled stdio bridge and the socket protocol are in -[`docs/MCP_CLIENT.md`](docs/MCP_CLIENT.md). When the chat page cannot be shown at all, the servers still -start and a notification carries the configuration to paste into your client. +[`docs/MCP_CLIENT.md`](docs/MCP_CLIENT.md). In Remote Development they run on the host, beside the project. ## Troubleshooting | Symptom | Usually | |---|---| -| The chat never loads, or the window is blank | JCEF is unavailable: below 2025.3.1 this version does not run; otherwise check `ide.browser.jcef.enabled` in the Registry | +| The chat never loads, or the window is blank | Below 2026.2 this version does not run; otherwise JCEF is unavailable: check `ide.browser.jcef.enabled` in the Registry (in Remote Development, on the client) | | "Claude Code was not found" with the binary installed | It is somewhere the plugin does not look, or the IDE did not inherit your `PATH`; paste the path into the card | | A tool call is refused with no card to override | The guard blocked it; the message names the rule and the Settings path. Foreign-territory blocks are absolute by design | -| Claude uses `Bash` or `grep` although the IDE tools exist | The flame is off, or a rule is: turn God Mode on, or the rule in Settings ▸ Claude Code ▸ Claude IDE Integration | +| Claude uses `Bash` or `grep` although the IDE tools exist | The flame is off, or a rule is: turn God Mode on, or the rule in Settings ▸ Claude Code ▸ Claude IDE Integration. In long sessions, the [skills and settings](#recommended-skills-and-settings) hook repeats the method on every prompt | | A domain is missing from the servers | The IDE plugin behind it is not installed or disabled (Git, GitHub, Java, Database, Terminal…) | | A commit or Services action answers "not enabled here" | The view had not been shown yet; ask again, the view is now open, or open it yourself | | Signed out after a restart | The credential could not be renewed; sign in again, and check the IDE reaches your keychain | @@ -463,7 +491,7 @@ Deeper cases with log locations: [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTIN ## Build from source -JDK 21 and Node 22+ (`.nvmrc`). `./gradlew buildPlugin` produces `build/distributions/claude-code-native-6.0.0.zip`; +JDK 25 and Node 24 (`.nvmrc`). `./gradlew buildPlugin` produces `build/distributions/claude-code-native-6.5.0.zip`; `./gradlew test` runs the JVM suite, `npm test` the frontend suite, `./gradlew detekt spotlessCheck` and `npm run lint` the static gates, `./gradlew verifyPlugin` the Plugin Verifier against the declared range. The rules the code is held to — no deprecated or internal platform API, a 250-line ceiling per file, no @@ -487,7 +515,7 @@ comments, one gateway file per external plugin, the guard off limits — are in GPL-3.0 — see [`LICENSE`](LICENSE) and [`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md). *Claude* and *Claude Code* are trademarks of Anthropic, PBC; *JetBrains* and the IDE names are trademarks of JetBrains s.r.o. This project is not affiliated with, sponsored by, or endorsed by either. The upstream repository is -[serialexperimentslainnnn/claude-code-for-jetbrains](https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains). +[serialexperimentslainnnn/claude-code-native](https://github.com/serialexperimentslainnnn/claude-code-native). ## Disclaimer diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index dea38d0c..f9eca228 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,61 @@ +## v6.5.0 — 2026-10-02 + +**The chat works in Remote Development.** Until now, in a remote setup the plugin loaded only on the host, where +the chat page cannot be drawn. The plugin is now split in two: the tool window and the embedded browser run +where you see them — in JetBrains Client — and the sessions, the IDE servers, the guard, your settings, Git and +the diffs run on the host, beside the project. The two talk over the platform's own RPC, so there is no port to +forward and nothing to configure, and a local IDE takes exactly the same path. Install the plugin on the host; +the client gets its copy from the Marketplace through plugin sync. + +**Code With Me: the host keeps its chat.** Guests get none. JetBrains is retiring Code With Me — 2026.1 was the +last release with official support — so the plugin does not build a guest experience on it. + +**This release requires 2026.2 (build 262.8665.258) or newer.** The RPC that joins the two halves is internal +in 2025.3 and 2026.1, and the plugin uses no internal API. On 2025.3.1 or 2026.1, stay on **6.0.1** — it keeps +working — or update the IDE. + +**Answers stream as they are written, and the chat stays light while they do.** A streaming answer used to +re-render the whole message every 30 ms; now only what arrived is appended, and running rows and live tool +output are patched in place. The IDE is spared work too: the file system is refreshed only after tools that +write, the page is assembled once per IDE, a restored chat is read from the end of its transcript, and prompts +reach the CLI off the UI thread. + +**Smaller answers from the IDE tools, so a session spends fewer tokens.** A batch of `read_file` calls shares +one budget, search results and project problems come grouped by file, and every limit a tool takes has a +ceiling. A command Claude runs in the IDE terminal now shows on its card as its own code block, ready to +copy, with the rest of the call's arguments apart. + +**Get the most out of the IDE tools.** Install +[Claude Code Native — Skills and Settings](https://github.com/serialexperimentslainnnn/claude-code-native-skills-and-settings) +into `~/.claude`. The plugin gives Claude the IDE's tools; that configuration tells Claude how to use them and +keeps it using them as a conversation grows: a working method repeated on every prompt, a skill that maps each +job to its IDE tool, a catalogue of engineering-standards skills and a set of orchestration workflows. + +**A round of bugs, gone.** The trust dialog no longer comes back every three seconds after you press *Cancel*. +A CLI that crashes on start is no longer restarted forever. Closed chat tabs no longer hold on to memory. The +IDE no longer freezes when `claude` stops reading its input. A prompt with attachments is no longer lost when +the session cannot start. One malformed message no longer takes an IDE server down. `run_configuration` +reports the exit code of the run it started, and a batch no longer applies half its items. Closing the last +chat tab shows the new one. A scratch file keeps the name you give it, even when one with the same name and +another extension exists. + +**The chat page works from the keyboard and with a screen reader.** Tool cards open and close from the +keyboard, menus and the attach menu can be driven without a mouse, toggles announce their state, pressing +Enter while an input method is composing no longer sends the prompt, and focus stays where it was when the +page redraws. + +**Security.** A renewed sign-in is no longer wiped when the keyring refuses to store it. The IDE servers admit +a client only after its process has started, and only for a short while. The token file is replaced +atomically. An API key is checked before it is accepted. The guard no longer holds up the conversation while +it decides, a permission card is answered once and redrawn when its content changes, each guard alert is +announced once, and the page forwards only web, `jb://` and relative links. + +**Under the hood.** DOMPurify 3.4.16, highlight.js 11.12.0 and marked 18.0.14 in the page; Kotlin 2.4 on a +JDK 25 toolchain, Gradle 9.7.1 and the IntelliJ Platform Gradle Plugin 2.19.0 in the build. The plugin no +longer bundles its own kotlinx-serialization and uses the platform's. The code that reaches Git, GitHub, Java, +Terminal, IntelliLang, Database, line bookmarks and the 2026.3 Problems view lives in optional modules that +load only when those plugins are present. + ## v6.0.1 — 2026-09-22 **`find_symbols` works in Rider again.** Asking Claude for a symbol there failed outright with diff --git a/SECURITY.md b/SECURITY.md index d03d1f5e..9c9ebac1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,8 +8,8 @@ seriously and follow responsible disclosure. | Version | Supported | |---------|--------------| -| 5.x | Yes (active) | -| < 5.0 | No | +| 6.x | Yes (active) | +| < 6.0 | No | Only the latest release of the current major receives security fixes. There is no backporting to earlier majors: the plugin ships through the JetBrains @@ -22,7 +22,7 @@ Please **do not** open a public GitHub issue, discussion, or Marketplace review for security problems. **Use GitHub's private vulnerability reporting:** -[Report a vulnerability](https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/security/advisories/new) +[Report a vulnerability](https://github.com/serialexperimentslainnnn/claude-code-native/security/advisories/new) (repository → **Security** → **Report a vulnerability**). This replaces the email address that used to be published here, and it is the diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 66487bd3..ebdf5d87 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -21,23 +21,21 @@ grant is silent. The vendored versions below are the ones read out of the shipped files themselves (`marked`'s and `highlight.js`'s embedded version strings, DOMPurify's `version` constant), not the ones named in a -banner. `marked.min.js` and `purify.min.js` were additionally confirmed **byte-identical** to the -upstream published `dist` for their stated version, which is what substantiates "redistributed -verbatim, unmodified" below; `highlight.min.js` is a curated subset build and so matches no upstream -artifact by construction. +banner. All three files were additionally confirmed **byte-identical** to the upstream published +artifact for their stated version (the npm tarball, integrity-checked against the registry), which is +what substantiates "redistributed verbatim, unmodified" below. The license texts referenced as `LICENSES/…` live at the repository root during development and are packaged into the artifact under `META-INF/licenses/` (see `build.gradle.kts`), alongside this file at `META-INF/THIRD-PARTY-NOTICES.md` and the project's own license at `META-INF/LICENSE`. The inventory is **complete against the artifact, not against the source tree**: the only files in -`claude-code-native-.zip` under `claude-code-native/lib/` are the plugin's own jar (which -carries the vendored web assets), the two kotlinx.serialization jars listed below, and the generated -`searchableOptions` jar. Everything with a third-party license in that list has an entry here. +`claude-code-native-.zip` under `claude-code-native/lib/` are the plugin's own jars (one of +which carries the vendored web assets) and the generated `searchableOptions` jar. Everything with a +third-party license in that list has an entry here. -Last verified: 2026-08-11, against the upstream `LICENSE` files at the pinned tags -(`markedjs/marked@v12.0.0`, `cure53/DOMPurify@3.4.13`, `highlightjs/highlight.js@11.11.2`, -`Kotlin/kotlinx.serialization@v1.7.3`). +Last verified: 2026-09-23, against the upstream `LICENSE` files at the pinned tags +(`markedjs/marked@v18.0.14`, `cure53/DOMPurify@3.4.16`, `highlightjs/highlight.js@11.12.0`). --- @@ -45,11 +43,10 @@ Last verified: 2026-08-11, against the upstream `LICENSE` files at the pinned ta These are vendored into the plugin's embedded web UI under `jcef/` and are served to the JCEF browser at runtime. Each is redistributed exactly as published upstream, with its license banner -intact and no edit of our own — verbatim for marked and DOMPurify, and for highlight.js the upstream -subset build as generated (see its entry). +intact and no edit of our own. -### marked — 12.0.0 -- **License:** `MIT AND BSD-3-Clause` — marked itself is MIT; its `LICENSE.md` additionally +### marked — 18.0.14 +- **License:** `MIT AND BSD-3-Clause` — marked itself is MIT; its `LICENSE` additionally reproduces the notice of the original **Markdown** (John Gruber, 2004), which is a BSD-3-Clause form license. Both are conditions of redistributing the file, so both are reproduced here. - **Copyright:** @@ -58,19 +55,18 @@ subset build as generated (see its entry). - Copyright © 2004, John Gruber (the Markdown notice) - **Project:** https://github.com/markedjs/marked - **Full text:** `LICENSES/MIT.txt` (marked) and `LICENSES/BSD-3-Clause-Markdown.txt` (Markdown) -- **Note:** the banner inside `marked.min.js` reads *"Copyright (c) 2011-2024, Christopher Jeffrey"* - — a single line that names neither MarkedJS nor Gruber and states a date range that does not - appear in the license. `LICENSE.md` at `v12.0.0` is the grant and is what is reproduced above. +- **Note:** `marked.min.js` is upstream's `lib/marked.umd.js`, the minified browser build that + replaced `marked.min.js` in marked 16, kept under the old name. Its banner names MarkedJS and + Christopher Jeffrey but not Gruber; `LICENSE` at `v18.0.14` is the grant and is what is + reproduced above. -### DOMPurify — 3.4.13 +### DOMPurify — 3.4.16 - **License:** `MPL-2.0 OR Apache-2.0` — dual-licensed, as upstream's own `package.json` states it - verbatim at this tag. At `3.4.13` the two grants live in two files: `LICENSE` carries the bare + verbatim at this tag. At `3.4.16` the two grants live in two files: `LICENSE` carries the bare Apache-2.0 text and `LICENSE-MPL` carries the MPL-2.0 text. - **License chosen by this project: Apache-2.0.** A dual `OR` license is a choice the redistributor must make and record; leaving it unstated is an - unmade decision. Apache-2.0 is selected because it is already the license of another component in - this artifact (kotlinx.serialization), so the artifact carries one fewer distinct license text, and - because Apache-2.0 grants patent rights explicitly whereas MPL-2.0's grant is narrower in scope. + unmade decision. Apache-2.0 is selected because it grants patent rights explicitly whereas MPL-2.0's grant is narrower in scope. MPL-2.0's per-file copyleft would also attach obligations if the file were ever modified — it is not, but choosing Apache-2.0 removes the question entirely. Because the choice is Apache-2.0, the MPL-2.0 text is deliberately **not** carried in `LICENSES/`. @@ -82,44 +78,29 @@ subset build as generated (see its entry). - **Note — this entry is the exception to the "read the `LICENSE`, not the banner" rule, and it is worth stating why.** Up to and including `3.0.11`, DOMPurify's `LICENSE` opened with a header naming the author (*"DOMPurify / Copyright 2023 Dr.-Ing. Mario Heiderich, Cure53"*) followed by the - dual-license statement, and that named individual was the notice to preserve. At `3.4.13` that + dual-license statement, and that named individual was the notice to preserve. At `3.4.16` that header is **gone**: `LICENSE` is the unmodified Apache-2.0 boilerplate, whose only copyright line is the appendix's unfilled `Copyright {yyyy} {name of copyright owner}` placeholder. So the sole copyright notice upstream still asserts is the banner inside `purify.min.js` — *"(c) Cure53 and other contributors"* — which the vendored file carries intact, as Apache-2.0 §4(c) requires. Both forms are reproduced above rather than picking one, because dropping the named form would discard a notice that upstream did assert, and it costs nothing to keep. -- **Verified:** the DOMPurify repository publishes **no `NOTICE` file** at tag `3.4.13`, so having +- **Verified:** the DOMPurify repository publishes **no `NOTICE` file** at tag `3.4.16`, so having chosen Apache-2.0 there is nothing further to propagate under Apache-2.0 §4(d). -### highlight.js — 11.11.2 +### highlight.js — 11.12.0 - **License:** BSD-3-Clause (`SPDX-License-Identifier: BSD-3-Clause`) - **Copyright:** Copyright (c) 2006, Ivan Sagalaev. All rights reserved. - **Project:** https://github.com/highlightjs/highlight.js - **Full text:** `LICENSES/BSD-3-Clause.txt` — byte-identical to the upstream `LICENSE` at this tag. -- **Note:** a curated subset build (37 bundled grammars), redistributed as built. The banner inside +- **Note:** the upstream common build (`@highlightjs/cdn-assets@11.12.0`, `highlight.min.js`, 36 + bundled grammars), redistributed verbatim. The banner inside `highlight.min.js` reads *"(c) 2006-2026 Josh Goebel <hello@joshgoebel.com> and other contributors"*, but the `LICENSE` at this tag still names only Ivan Sagalaev — so the copyright above is the license's, not the banner's, and it has not changed across the versions vendored here. --- -## Shipped as separate jars in `lib/` - -### kotlinx.serialization (`kotlinx-serialization-core-jvm`, `kotlinx-serialization-json-jvm`) — 1.7.3 -- **License:** Apache-2.0 (`SPDX-License-Identifier: Apache-2.0`) -- **Copyright:** Copyright 2017-2024 JetBrains s.r.o. -- **Project:** https://github.com/Kotlin/kotlinx.serialization -- **Full text:** `LICENSES/Apache-2.0.txt` -- **Verified:** the published jars carry no `META-INF/LICENSE` **and no `META-INF/NOTICE`** (checked - in `kotlinx-serialization-core-jvm-1.7.3.jar` and `-json-jvm-1.7.3.jar`), so the license was read - from the project's `LICENSE.txt` at tag `v1.7.3` rather than inferred from the artifact. That file - is the bare Apache-2.0 text with no copyright line appended; the copyright above is the one the - project's own source headers carry (`Copyright 2017- JetBrains s.r.o.`, latest year 2024). - The repository publishes **no `NOTICE` file**, so Apache-2.0 §4(d) adds no obligation here. - ---- - ## Not redistributed The following are used during development or referenced as documentation and are **not** part of the @@ -131,3 +112,5 @@ published artifact, so they create no redistribution obligation here: - **The `claude` CLI itself** — a separate program the user installs and licenses independently. The plugin executes it; it does not redistribute it. - **The IntelliJ Platform** — provided by the host IDE at runtime, not bundled. +- **kotlinx.serialization** — the plugin uses the copy the IntelliJ Platform ships, and bundles no + jar of its own. diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 00000000..fbdf7b83 --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,22 @@ +sourceSets.main { + kotlin.srcDir(rootProject.file("src/main/kotlin")) + java.srcDir(rootProject.file("src/main/java")) + resources.srcDir(rootProject.file("src/main/resources")) + resources.exclude("META-INF/plugin.xml", "META-INF/pluginIcon*.svg") +} + +dependencies { + intellijPlatform { + bundledModules( + "intellij.platform.backend", + "intellij.platform.kernel.backend", + "intellij.platform.rpc.backend", + "intellij.platform.vcs.impl", + "intellij.platform.vcs.log", + "intellij.platform.vcs.log.impl", + "intellij.platform.smRunner", + ) + bundledPlugin("Git4Idea") + } + implementation(project(":shared")) +} diff --git a/backend/src/main/resources/dev.lain.claudejb.backend.xml b/backend/src/main/resources/dev.lain.claudejb.backend.xml new file mode 100644 index 00000000..d99cf58d --- /dev/null +++ b/backend/src/main/resources/dev.lain.claudejb.backend.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bookmarks/build.gradle.kts b/bookmarks/build.gradle.kts new file mode 100644 index 00000000..dadc7058 --- /dev/null +++ b/bookmarks/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + intellijPlatform { + bundledModules( + "intellij.platform.backend", + "intellij.platform.bookmarks", + ) + } + implementation(project(":backend")) +} diff --git a/bookmarks/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ProviderLineBookmarks.kt b/bookmarks/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ProviderLineBookmarks.kt new file mode 100644 index 00000000..1d7d6b71 --- /dev/null +++ b/bookmarks/src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ProviderLineBookmarks.kt @@ -0,0 +1,11 @@ +package dev.lain.claudejb.controller.mcp.tools.code + +import com.intellij.ide.bookmark.Bookmark +import com.intellij.ide.bookmark.providers.LineBookmarkProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile + +internal class ProviderLineBookmarks(private val project: Project) : LineBookmarks { + override fun create(file: VirtualFile, line: Int): Bookmark? = + LineBookmarkProvider.Util.find(project)?.createBookmark(file, line) +} diff --git a/bookmarks/src/main/resources/dev.lain.claudejb.bookmarks.xml b/bookmarks/src/main/resources/dev.lain.claudejb.bookmarks.xml new file mode 100644 index 00000000..af404812 --- /dev/null +++ b/bookmarks/src/main/resources/dev.lain.claudejb.bookmarks.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 24573fd0..4ff06e46 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,384 +1,181 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType -import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform import org.jetbrains.intellij.platform.gradle.models.ProductRelease import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask +import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware +import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension plugins { - kotlin("jvm") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" - // PINNED AT 2.16.0 DELIBERATELY. 2.18.1 exists and the build warns about it on every run, but bumping it - // hangs the headless suite: `ChatSessionManagerHeadlessTest` never starts, because - // BasePlatformTestCase.setUp → LightPlatformTestCase.doSetup → IndexingTestUtil.waitUntilIndexesAreReady - // waits forever (confirmed by thread dump — the EDT sits in that frame; our code is never reached). The - // bump changes which platform test-framework is resolved, so this is a fixture-level regression, not ours - // to fix from here. Re-attempt as its own change, with the headless suite as the acceptance test — NOT as - // a drive-by inside a release branch, which is exactly how it got in and straight back out. - id("org.jetbrains.intellij.platform") version "2.16.0" - // Coverage, gated per package — see the `kover { }` block near the bottom for the thresholds and why they - // differ by package. (Until 5.0.0 this comment claimed a "≥90% target documented in - // docs/RELEASE_CHECKLIST.md". That document says nothing about coverage, and the real figure was 53%. A - // number nobody measured, pointing at a requirement that did not exist.) - id("org.jetbrains.kotlinx.kover") version "0.9.9" - // Static analysis (detekt) and formatting (ktlint via Spotless). Added in 5.0.0: until then the whole - // quality bar rested on review, which is exactly the thing the standards say to mechanise — "if format - // is being discussed in a review, a formatter is missing". - id("io.gitlab.arturbosch.detekt") version "1.23.8" - id("com.diffplug.spotless") version "8.9.0" + id("org.jetbrains.intellij.platform") + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlinx.kover") + id("dev.detekt") + id("com.diffplug.spotless") } group = "dev.lain" -version = "6.0.1" - -repositories { - mavenCentral() - intellijPlatform { - defaultRepositories() +version = "6.5.0" + +val platformBuild = "262.8665.258" +val serialization = "1.9.0" +val pluginModules = + listOf("shared", "frontend", "backend", "git", "github", "java", "intellilang", "terminal", "bookmarks", "database", "problems") + +allprojects { + plugins.withId("org.jetbrains.kotlin.jvm") { + extensions.configure { + jvmToolchain(25) + compilerOptions { + allWarningsAsErrors.set(true) + jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY) + } + } + } + plugins.withId("org.jetbrains.kotlinx.kover") { + extensions.configure { + useJacoco("0.8.15") + } + } + tasks.withType().configureEach { + exclude("**/PROJECTMAP.md") } } -// --------------------------------------------------------------------------- -// Test layout (the pyramid in docs/ + the plan): -// src/test, package `…` and `…headless`/`…integration` → `test` (unit) + `integrationTest` (headless+fake-claude) -// src/uiTest → `uiTest` (RemoteRobot, drives an external IDE; gated) -// Headless/integration tests live in src/test so they inherit the IntelliJ Platform classpath the plugin -// already wires for the `test` task (a custom source set does NOT get `Project` on its classpath). The -// `integrationTest` task simply re-runs the test classes filtered to the heavy packages; `test` excludes them. -// uiTest is a real separate source set: it talks to a running IDE over HTTP (remote-robot), so it must NOT -// pull the platform into its own classpath. -// --------------------------------------------------------------------------- -sourceSets { - create("uiTest") { - compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output - runtimeClasspath += output + compileClasspath - kotlin.srcDir("src/uiTest/kotlin") - resources.srcDir("src/uiTest/resources") +subprojects { + apply(plugin = "org.jetbrains.intellij.platform.module") + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + apply(plugin = "rpc") + apply(plugin = "org.jetbrains.kotlinx.kover") + extensions.configure { + archivesName.set("dev.lain.claudejb.$name") + } + dependencies { + "compileOnly"("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:$serialization") + "compileOnly"("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$serialization") } } -configurations { - named("uiTestImplementation") { extendsFrom(configurations.testImplementation.get()) } - named("uiTestRuntimeOnly") { extendsFrom(configurations.testRuntimeOnly.get()) } +sourceSets.main { + kotlin.setSrcDirs(emptyList()) + java.setSrcDirs(emptyList()) + resources.setSrcDirs(listOf("src/main/resources")) + resources.include("META-INF/**") } +apply(from = "gradle/test-suites.gradle.kts") +apply(from = "gradle/quality.gradle.kts") +apply(from = "gradle/coverage.gradle.kts") +apply(from = "gradle/release-notes.gradle.kts") + dependencies { intellijPlatform { - // Compile against IntelliJ IDEA Community 2025.3 — the declared since-build floor (253), so we build - // against the oldest IDE we support (never below it) and the plugin still loads in newer IDEs because - // untilBuild is widened below. - // - // Raised from 2025.2 together with the floor: `com.intellij.modules.jcef`, which the descriptor now - // declares, does not exist in 252 at all — compiling against an IDE that cannot satisfy the plugin's - // own dependencies makes `runIde` a sandbox the plugin refuses to load in, and the "build against the - // floor" rule stops meaning anything. - // By BUILD NUMBER, not "2025.3.1": that marketing version is not published in the Maven repository the - // plugin resolves from (only the point releases are), so the plain name fails to resolve. - // `useInstaller = false` resolves the Maven artifact instead of the `.tar.gz` installer — smaller, and - // it carries everything this build needs, `com.intellij.modules.jcef` included (checked in the - // artifact's own `lib/product-backend.jar`). - // NOT `IntellijIdeaCommunity`: the `ideaIC` artifact stopped being published at 2025.3 (253) — the very - // floor this release moved to — and the Gradle plugin warns about it on every build. JetBrains ships a - // single unified IDEA distribution from 253 onwards, which is what `intellijIdea(…)` resolves. - // - // **253.29346.138 (2025.3.1), not 253.28294.334 (2025.3) — and the ten days between them are the whole - // point.** `com.intellij.modules.jcef`, which `plugin.xml` declares a MANDATORY dependency on, does not - // exist in 2025.3: its `product-backend.jar` carries 38 `com.intellij.modules.*` aliases and none of - // them is that one. It appears in 2025.3.1 — 39 aliases, the extra one being exactly `jcef`. So on - // 2025.3 the IDE refuses to load this plugin outright ("has dependency on 'com.intellij.modules.jcef' - // which is not installed"), which is the same failure that made 5.1.1 dead on 2026.2, at the other end - // of the range. The dependency cannot simply be dropped: from 262 JCEF is a bundled plugin, and without - // declaring it the plugin's classloader has no `com.intellij.ui.jcef.*` at all. It is mandatory from - // the build that has it and impossible before — so the FLOOR moves, and `sinceBuild` below moves with - // it. (On 2025.3 itself `JBCefApp` does live in `lib/app.jar`, i.e. everything compiles and 5.1.1 ran - // there happily; it is the declaration that cannot be satisfied, not the classes that are missing.) - intellijIdea("253.29346.138") { + intellijIdea(platformBuild) { useInstaller = false } - // Bundled IDE Terminal: used to open an interactive `claude login` session (the OAuth flow needs a - // TTY, which the stream-json process doesn't have). Compile-only coupling; TerminalLauncher guards - // its use behind PluginManager.isPluginInstalled so a disabled Terminal plugin degrades gracefully. - bundledPlugin("org.jetbrains.plugins.terminal") - // Bundled Git plugin: compile-only coupling for `git4idea.*` (GitRepositoryManager, GitHistoryUtils), - // read-only. Declared OPTIONAL in META-INF/plugin.xml (config-file claude-git.xml) — unlike JCEF, an IDE - // without Git, or a project that is not a working copy, must still load the plugin; `GitGateway` is the - // only file that names a git4idea type and it is never reached unless `GitAvailability` says yes. - bundledPlugin("Git4Idea") - // Bundled GitHub plugin: compile-only coupling for the pull-request data (`GHAccountsUtil`, the API - // executor, `GHGQLRequests`). OPTIONAL in META-INF/plugin.xml (config-file claude-github.xml); - // `GitHubGateway` is the only file that names an org.jetbrains.plugins.github type and checks the - // plugin before touching it, so an IDE without it answers "not available" instead of dying. - bundledPlugin("org.jetbrains.plugins.github") - // Bundled Java plugin: compile-only coupling for UAST (the unified AST over Java, Kotlin, Scala, Groovy), - // which ships inside it. OPTIONAL in META-INF/plugin.xml (config-file claude-java.xml): PyCharm and the - // other IDEs without Java load the plugin without the uast domain; UastTools is the only file naming - // org.jetbrains.uast and the catalog row checks JavaAvailability before touching it. - bundledPlugin("com.intellij.java") + pluginModules.forEach { pluginModule(implementation(project(":$it"))) } + bundledModules( + "intellij.platform.frontend", + "intellij.platform.backend", + "intellij.platform.kernel.backend", + "intellij.platform.rpc.backend", + ) + bundledPlugins( + "com.intellij.modules.jcef", + "org.jetbrains.plugins.terminal", + "Git4Idea", + "org.jetbrains.plugins.github", + "com.intellij.java", + "com.intellij.database", + ) + bundledModule("org.intellij.intelliLang") + testFramework(TestFrameworkType.Platform) } - - // JSON (de)serialization for the stream-json / control protocol. - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - - // Unit tests (pure JVM: protocol parsing/building, no IntelliJ Platform fixtures needed). - testImplementation(platform("org.junit:junit-bom:6.1.2")) + pluginModules.forEach { testImplementation(project(":$it")) } + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization") + testImplementation(platform("org.junit:junit-bom:6.1.3")) testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - // JUnit4/3 on the COMPILE classpath: the plugin's test executor references JUnit4 API, and - // BasePlatformTestCase (headless component tests) descends from JUnit3 `junit.framework.TestCase`. testImplementation("junit:junit:4.13.2") - - // Headless/integration tests (in src/test) use BasePlatformTestCase, which descends from JUnit3 TestCase; - // the vintage engine lets the JUnit Platform discover and run them alongside the JUnit5 unit tests. + testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") - // Full IntelliJ Platform test fixtures (BasePlatformTestCase, LightVirtualFile, EDT helpers). - intellijPlatform { - testFramework(TestFrameworkType.Platform) - } - - // --- uiTest: RemoteRobot end-to-end (Layer D), gated by -PuiTest.enabled=true --- - "uiTestImplementation"(platform("org.junit:junit-bom:6.1.2")) - "uiTestImplementation"("org.junit.jupiter:junit-jupiter") - "uiTestRuntimeOnly"("org.junit.platform:junit-platform-launcher") - "uiTestImplementation"("com.intellij.remoterobot:remote-robot:0.11.23") - "uiTestImplementation"("com.intellij.remoterobot:remote-fixtures:0.11.23") - "uiTestImplementation"("com.squareup.okhttp3:okhttp:4.12.0") } -// The Kotlin stdlib and JetBrains annotations are provided by the IntelliJ Platform at runtime; keep -// them out of the bundled plugin (where they would shadow the platform copies and trip the verifier). -// runtimeClasspath is the configuration the plugin is assembled from. -configurations.named("runtimeClasspath") { - exclude(mapOf("group" to "org.jetbrains.kotlin", "module" to "kotlin-stdlib")) - exclude(mapOf("group" to "org.jetbrains", "module" to "annotations")) -} +val npm = if (System.getProperty("os.name").startsWith("Windows")) "npm.cmd" else "npm" +val fakeClaude: String = file("bin/fake-claude").absolutePath tasks { - // Attribution travels INSIDE the artifact (opensource-licensing-standards §5.4). - // - // The published zip redistributes third-party code: marked, DOMPurify and highlight.js are vendored into - // the plugin jar under `jcef/`, and kotlinx.serialization ships as its own jars. MIT, BSD-3-Clause and - // Apache-2.0 all require the copyright notice and licence text to be preserved *on redistribution* — and a - // file sitting in the Git repository does not accompany the binary a user installs from the Marketplace. - // Copying them into the jar's resources is what actually discharges the obligation. - // - // Kept as a build step rather than a checked-in copy under `src/main/resources/`, so the notices cannot - // drift out of sync with the files they describe: one source of truth at the repository root, packaged at - // build time. `THIRD-PARTY-NOTICES.md` is surfaced to the user by the About dialog (see InfoDialogs). - val npm = if (System.getProperty("os.name").startsWith("Windows")) "npm.cmd" else "npm" - - val npmInstall by registering(Exec::class) { - inputs.files("package.json", "package-lock.json") - outputs.file("node_modules/.package-lock.json") - commandLine(npm, "ci") - } - - val compileWeb by registering(Exec::class) { - dependsOn(npmInstall) - inputs.dir("src/main/ts/jcef") - inputs.file("tsconfig.json") - outputs.dir(layout.buildDirectory.dir("web")) - doFirst { delete(layout.buildDirectory.dir("web")) } - commandLine(npm, "run", "build") - } - - val frontendTest by registering(Exec::class) { - dependsOn(compileWeb) - inputs.dir("src/test/frontend") - inputs.dir("src/main/resources/jcef") - inputs.dir("src/main/ts/jcef") - outputs.dir(layout.buildDirectory.dir("reports/frontend")) - environment("CI", "true") - commandLine(npm, "test") - } - + val frontendTest = + register("frontendTest") { + dependsOn(":frontend:compileWeb") + inputs.dir("src/test/frontend") + inputs.dir("frontend/src/main/resources/jcef") + inputs.dir("frontend/src/main/ts/jcef") + outputs.dir(layout.buildDirectory.dir("reports/frontend")) + environment("CI", "true") + commandLine(npm, "test") + } check { dependsOn(frontendTest) } - + compileTestKotlin { + friendPaths.from(pluginModules.map { project(":$it").layout.buildDirectory.dir("classes/kotlin/main") }) + friendPaths.from( + pluginModules.map { + project(":$it") + .layout.buildDirectory + .dir("libs") + .map { libs -> libs.asFileTree } + }, + ) + } processResources { - from(compileWeb) - // The distributed map is written FOR this repository and lands in the artifact by accident: the one - // under `src/main/resources/jcef/` is a resource like any other, so it shipped inside the plugin jar — - // 20 KB of internal design notes and source paths handed to every user, for nothing. Excluded by - // pattern rather than by path, because the next directory to get a map will be a `resources` one again - // and nobody will think of this file. Nothing reads it at runtime: `JcefHost` names every script and - // stylesheet it loads explicitly (`appNames`, `CSS_PARTS`) and globs no resource directory. - exclude("**/PROJECTMAP.md") - from(rootProject.file("THIRD-PARTY-NOTICES.md")) { into("META-INF") } - from(rootProject.file("LICENSE")) { into("META-INF") } - from(rootProject.file("LICENSES")) { into("META-INF/licenses") } + from(file("THIRD-PARTY-NOTICES.md")) { into("META-INF") } + from(file("LICENSE")) { into("META-INF") } + from(file("LICENSES")) { into("META-INF/licenses") } } runIde { jvmArgs("-Djb.privacy.policy.text=", "-Djb.consents.confirmation.enabled=false", "-Dclaudejb.debug=true") } test { - // Exclude the live drift check: it downloads the latest SDK from npm and spawns the real binary, - // so it must not run in the default suite. It lives in the `checkDrift` task below. (excludeTags only - // affects JUnit5/jupiter discovery — the JUnit3 vintage headless tests carry no tags and still run.) - useJUnitPlatform { excludeTags("driftLive") } - // Runs the whole non-UI pyramid: unit (jupiter) + headless/integration (BasePlatformTestCase via the - // vintage engine). The IntelliJ Platform Gradle plugin only instruments ITS `test` task with the - // platform runtime, so headless tests must run here rather than in a hand-rolled Test task. - systemProperty("claudejb.fakeClaude", rootProject.file("bin/fake-claude").absolutePath) + useJUnitPlatform { excludeTags("driftLive", "bench") } + filter { excludeTestsMatching("*Bench") } + systemProperty("claudejb.fakeClaude", fakeClaude) } +} - // On-demand protocol drift watcher (NOT wired into `check`). Downloads the latest published SDK and - // probes the locally-installed (auto-updated) `claude` binary, then prints an agent-consumable report - // and fails on real surface drift. Runs only the `driftLive`-tagged DriftLiveCheck against the test - // classpath (the pure extraction/diff logic is covered offline by DriftDetectorTest in the normal suite). - // ./gradlew checkDrift # uses ~/.local/bin/claude - // ./gradlew checkDrift -PclaudeBinary=/path # or CLAUDE_BINARY env var - val checkDrift by registering(Test::class) { - description = "Download latest SDK + probe the installed binary; report protocol drift (on-demand)." +intellijPlatformTesting.testIde.register("bench") { + testFrameworks(TestFrameworkType.Platform) + task { group = "verification" - // Only the jupiter engine: the vintage engine would try to DISCOVER (instantiate) the JUnit3 - // headless BasePlatformTestCase classes, which aren't on this task's classpath (only the plugin's - // own `test` task gets the platform runtime) — that fails before tag filtering even applies. - useJUnitPlatform { - includeTags("driftLive") - includeEngines("junit-jupiter") + description = "Runs the benchmarks and keeps their figures in build/bench/results.txt." + useJUnitPlatform() + filter { + includeTestsMatching("*Bench") + includeTestsMatching("dev.lain.claudejb.bench.*") } - // Belt-and-suspenders: restrict discovery to the drift package. - filter { includeTestsMatching("dev.lain.claudejb.drift.*") } - testClassesDirs = - sourceSets.test - .get() - .output.classesDirs - classpath = sourceSets.test.get().runtimeClasspath - // Always re-run (it polls the network + binary); never serve a cached result. - outputs.upToDateWhen { false } - val binaryPath = ( - providers.gradleProperty("claudeBinary").orNull - ?: providers.environmentVariable("CLAUDE_BINARY").orNull - ?: "${System.getProperty("user.home")}/.local/bin/claude" - ) - systemProperty("claudejb.drift.projectDir", rootProject.projectDir.absolutePath) - systemProperty( - "claudejb.drift.sdkDir", - rootProject.file("node_modules/@anthropic-ai/claude-agent-sdk").absolutePath, - ) - systemProperty("claudejb.drift.binary", binaryPath) - systemProperty("claudejb.drift.baseline", rootProject.file("scripts/drift-baseline.properties").absolutePath) - // Surface the report (println from the test) on the console. + workingDir(layout.projectDirectory) + systemProperty("claudejb.fakeClaude", fakeClaude) testLogging { showStandardStreams = true } - } - - // NB there is deliberately NO `checkProjectMap` task, and the `PROJECTMAP.md` files are not gated. - // - // They are an orientation index for AI-assisted sessions — a local tooling convention, not part of the - // product: nothing in them reaches the artifact, and `processResources` excludes them from it. Gating the - // build on them made the one check whose failure can never be a defect in the plugin, and imposed a - // Python script on anyone who clones the repository and edits a file. Regenerate with - // `python3 scripts/gen-projectmap.py` when you want them current. - - // Convenience alias: run only the heavy IntelliJ-fixture packages (headless + fake-claude integration). - val integrationTest by registering { - description = "Runs only the headless + fake-claude integration tests (subset of `test`)." - group = "verification" - finalizedBy(named("test")) - doFirst { - (named("test").get() as Test).filter { - includeTestsMatching("dev.lain.claudejb.headless.*") - includeTestsMatching("dev.lain.claudejb.integration.*") - } - } - } - - val uiTest by registering(Test::class) { - description = "End-to-end UI tests driving the IDE via RemoteRobot (Layer D)." - group = "verification" - useJUnitPlatform() - testClassesDirs = sourceSets["uiTest"].output.classesDirs - classpath = sourceSets["uiTest"].runtimeClasspath - // RemoteRobot's HTTP client (Retrofit + Gson) reflects into JDK-internal fields to (de)serialize - // responses/exceptions; under JDK 17+ strong encapsulation that throws InaccessibleObjectException - // unless we open the relevant java.base packages to the (unnamed) test module. - jvmArgs( - "--add-opens=java.base/java.lang=ALL-UNNAMED", - "--add-opens=java.base/java.util=ALL-UNNAMED", - "--add-opens=java.base/java.text=ALL-UNNAMED", - "--add-opens=java.desktop/java.awt=ALL-UNNAMED", - "--add-opens=java.desktop/java.awt.event=ALL-UNNAMED", - ) - shouldRunAfter("integrationTest") - // Let a remote runner override where the robot-server lives (defaults to 127.0.0.1:8082 in UiTestBase). - System.getProperty("robot-server.url")?.let { systemProperty("robot-server.url", it) } - // RemoteRobot needs a running IDE on a display, and this task starts neither, so the flag is an - // acknowledgement that both are already up. It is asserted rather than used as an `onlyIf`: a Gradle - // skip is `BUILD SUCCESSFUL` with zero tests executed, which is the one outcome a verification task - // must never produce. `uiTest` hangs off no aggregate task (see `check` below), so the only way to - // reach this is to ask for it by name — and asking for it without the flag is a mistake worth a red. - doFirst { - if (project.findProperty("uiTest.enabled") != "true") { - throw GradleException( - "uiTest needs an IDE already running with robot-server on a display, and does not start " + - "one. Boot it with `./gradlew runIdeForUiTests` (under xvfb-run if headless), then " + - "re-run this task with -PuiTest.enabled=true.", - ) - } + val results = layout.buildDirectory.file("bench/results.txt") + outputs.file(results) + outputs.upToDateWhen { false } + doFirst { results.get().asFile.delete() } + doLast { + val file = results.get().asFile + logger.lifecycle(if (file.exists()) file.readText() else "No benchmark wrote a result.") } } - - // The uiTest source set inherits the test classpath, so the sandbox-project fixture can be contributed - // from more than one resource root; tolerate the duplicate deterministically instead of failing the copy. - named("processUiTestResources") { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } - - // `check` already depends on `test`, which now includes the headless/integration packages. - // - // `uiTest` is kept out of TWO graphs, and both are needed — enumerating one and stopping there invites the - // conclusion that it is enough, which it is not: - // 1. out of `check`, because it needs a display and an already-running IDE, so it runs nightly or by - // hand and would otherwise fail every ordinary `check`; - // 2. out of Kover's report graph, via `disabledForTestTasks` in the `kover { }` block below — a `Test` - // task is pulled in as a dependency of `koverXmlReport`/`koverVerify` whether or not `check` wants - // it, so staying out of `check` alone leaves the coverage tasks unable to run anywhere the IDE is - // not already up. } -// --------------------------------------------------------------------------- -// RemoteRobot harness (Layer D). `intellijPlatformTesting.runIde` is the canonical 2.x DSL for a custom -// IDE-under-test: it builds a sandbox, installs extra plugins (here `robotServerPlugin()`, which exposes the -// HTTP endpoint RemoteRobot talks to), and lets us tune the JVM. We launch it on port 8082 and point the -// plugin at `bin/fake-claude` via the two `claudejb.fake*` system properties (read by ClaudeSettings only -// when those props are present — a no-op in shipped IDEs). -// -// Flow (two terminals / two background steps — the IDE must be UP before the client tests connect): -// 1. ./gradlew runIdeForUiTests # starts the IDE with robot-server on :8082 (keep it running) -// 2. ./gradlew uiTest -PuiTest.enabled=true # the RemoteRobot client suite connects to :8082 -// Headless CI: wrap step 1 in `xvfb-run`. See docs/UI_TESTING.md. -// --------------------------------------------------------------------------- intellijPlatformTesting { runIde { register("runIdeForUiTests") { - // Install the robot-server plugin into this IDE's sandbox; that's what RemoteRobot drives. - plugins { - robotServerPlugin() - } + plugins { robotServerPlugin() } task { - // Open the minimal sandbox project so the IDE doesn't sit on the "open a project" screen — the - // tool window, composer and editor context only exist with a Project. We open a tiny build-less - // project (not this repo) to keep the suite fast and free of Gradle-import/trust prompts. The - // IDE opens the directory passed as the first positional CLI arg. - args(rootProject.file("src/uiTest/resources/sandbox-project").absolutePath) + args(file("src/uiTest/resources/sandbox-project").absolutePath) jvmArgs( - // RemoteRobot endpoint (UiTestBase connects to http://127.0.0.1:8082). "-Drobot-server.port=8082", - // THE WHOLE UI IS A BROWSER, and this is what lets the suite talk to it. Not a magic - // number: `ide.browser.jcef.jsQueryPoolSize` is a platform REGISTRY key — `JBCefClient` - // reads it once via `RegistryManager.intValue(...)` into `JS_QUERY_POOL_DEFAULT_SIZE`, and - // a registry value falls back to the system property of the same name (verified in the - // bytecode of `RegistryValue`/`JBCefClient` in the IDE distribution), so a `-D` on the - // IDE's command line IS how it is set. - // A `JBCefJSQuery` can only be attached to a browser that has ALREADY loaded if its slot - // was reserved when the client was created; with the pool at its default the platform - // refuses with "Set the property JBCefClient.Properties.JS_QUERY_POOL_SIZE to use - // JBCefJSQuery after the browser has been created". That is exactly what JetBrains' - // `JCefBrowserFixture` does, and it is the only route src/uiTest has into the DOM — so - // without this line every DOM-driving test fails at fixture construction, before it can - // assert anything. 10000 is the size the fixture's own documented precondition asks for - // (recorded again in `UiTestBase`'s KDoc); it costs reserved callback slots in a - // throwaway sandbox IDE and nothing else. Do not "tidy" it away. "-Dide.browser.jcef.jsQueryPoolSize=10000", - // Quiet, deterministic first run: no privacy/consent gates, no tips, no "what's new". "-Djb.privacy.policy.text=", "-Djb.consents.confirmation.enabled=false", "-Dide.show.tips.on.startup.default.value=false", @@ -386,19 +183,10 @@ intellijPlatformTesting { "-Dide.mac.file.chooser.native=false", "-DjbScreenMenuBar.enabled=false", "-Dapple.laf.useScreenMenuBar=false", - // Auto-trust opened projects so no "Trust this project?" modal blocks the robot. The key is - // `idea.` — the neighbour above is `ide.` because it is a platform REGISTRY key, and this one - // is a system property read by `TrustedProjects` via `Boolean.getBoolean`. The two - // namespaces sit ten lines apart, so the wrong prefix reads as consistent with its - // neighbour: verified against the 253 distribution, where `ide.trust.all.projects` appears - // nowhere. Under Xvfb there is a real display, so the headless escape hatch - // (`idea.trust.headless.disabled`) never fires and the modal has nothing to dismiss it. "-Didea.trust.all.projects=true", "-Dide.show.new.ui.welcome.screen=false", - // Point the plugin at the deterministic fake binary + default fixture (per-test scenarios - // can override FAKE_FIXTURE; see docs/UI_TESTING.md). Read by ClaudeSettings test hook. - "-Dclaudejb.fakeClaude=${rootProject.file("bin/fake-claude").absolutePath}", - "-Dclaudejb.fakeFixture=${rootProject.file("src/test/resources/fixtures/multi_message.jsonl").absolutePath}", + "-Dclaudejb.fakeClaude=$fakeClaude", + "-Dclaudejb.fakeFixture=${file("src/test/resources/fixtures/multi_message.jsonl").absolutePath}", ) } } @@ -406,40 +194,15 @@ intellijPlatformTesting { } intellijPlatform { + projectName = "claude-code-native" + pluginInstallationTarget = SplitModeAware.PluginInstallationTarget.BOTH pluginConfiguration { - // id/name/vendor/description live in META-INF/plugin.xml; only compatibility range is set here. ideaVersion { - // Floor 253 (2025.3), raised from 251 in 5.5.0 — and it is JCEF that raises it, not an API tidy-up. - // - // Since 262 the platform ships the embedded browser as a separate bundled plugin, so a plugin that - // wants `com.intellij.ui.jcef.*` in its classloader must declare `com.intellij.modules.jcef` (see - // META-INF/plugin.xml). That id does not exist on 251/252 — verified in the IDE distributions - // themselves: on 251/252 `JBCefApp` sits in `lib/app-client.jar` and nothing declares the module; - // on 253 and 261 the platform declares `` in - // `product-backend.jar`; on 262 it is the plugin. Declaring it therefore costs 2025.1 and 2025.2, - // and the alternative was leaving the plugin DEAD on 2026.2 — the whole UI is that browser, so - // there is nothing to degrade to. - // - // (The previous floor note still holds for the API: `FileChooserDescriptorFactory.multiFiles()` - // does not exist before 251. It is simply no longer the binding constraint.) - // - // Ceiling 263.*: declared ahead of the 2026.3 branch on purpose, so an EAP user is never locked out - // by a range we forgot to widen. It is not a guess — `verifyPlugin` verifies against the EAP and RC - // channels up to that bound (see the `select` block below), so the claim is checked on every run. - // 253.29346.138 = IntelliJ IDEA 2025.3.1, the FIRST build that ships - // `com.intellij.modules.jcef` — the mandatory dependency this plugin declares. 2025.3 itself - // (253.28294.334, ten days earlier) does not have it, and there the IDE refuses to load the plugin - // at all. A floor of plain "253" was therefore a promise that could not be kept for the first - // release of that branch; see the platform declaration above for the full reasoning. - sinceBuild = "253.29346.138" + sinceBuild = platformBuild untilBuild = "263.*" } - // "What's new" on the Marketplace = the latest version section of RELEASE_NOTES.md, as HTML. - changeNotes = provider { latestReleaseNotesHtml() } + changeNotes = provider { project.extra["changeNotesHtml"] as String } } - - // Marketplace publishing + plugin signing. All credentials come from the environment (GitHub Actions - // secrets); never commit them. Locally these are simply absent and the publish/sign tasks aren't run. publishing { token = providers.environmentVariable("PUBLISH_TOKEN") } @@ -448,34 +211,9 @@ intellijPlatform { privateKey = providers.environmentVariable("PRIVATE_KEY") password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") } - pluginVerification { - // 'JetBrains' in the plugin name is a Marketplace naming lint, not an API problem; muting it lets - // the verifier proceed to the actual binary-compatibility / internal-API checks we care about. freeArgs = listOf("-mute", "TemplateWordInPluginName") externalPrefixes = listOf("org.jetbrains.uast") - - // The "zero deprecations" rule, ENFORCED rather than merely written down. - // - // The plugin's default failure level is COMPATIBILITY_PROBLEMS + INTERNAL_API_USAGES + - // OVERRIDE_ONLY_API_USAGES — deprecated usages are only REPORTED. So this repo's stated policy - // ("never ship a deprecated or scheduled-for-removal API — treat it as a blocker, not a warning") - // was a promise a human had to keep by reading logs, and a rule that lives only in prose is not a - // rule. Adding DEPRECATED_API_USAGES is what makes the sentence true. - // - // EXPERIMENTAL_API_USAGES is deliberately NOT here, and that is a decision rather than an oversight: - // `DiffTabCleanup` uses `ProjectCloseListener.projectClosingBeforeSave` knowingly, because it is the - // only hook that runs BEFORE the workspace state is written — which is the whole point of it. An - // experimental API is acceptable with a reason; a deprecated one is not acceptable at all, because - // it has an announced removal date and the plugin has to keep working across the IDE range. - // - // MISSING_DEPENDENCIES is deliberately NOT here either. A mandatory `` the target IDE cannot - // satisfy means the plugin does not load at all, and the verifier does detect it — but the Gradle plugin - // (2.16.0, and 2.18.1 alike) parses the verifier's stdout by the "Missing dependencies" heading and - // cannot tell `(optional): Unavailable` from a mandatory gap, so with that level on, every PyCharm - // target fails on the optional com.intellij.modules.java dependency that PyCharm lacks by design. - // The protection that level gave lives in PluginDependenciesContractTest instead: every non-optional - // must be a platform module every IntelliJ-based IDE ships. failureLevel = listOf( VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS, @@ -484,370 +222,28 @@ intellijPlatform { VerifyPluginTask.FailureLevel.DEPRECATED_API_USAGES, ) ides { - // No hardcoded path in the repo: a developer can point the verifier at local IDE installs to skip the - // downloads, via -PlocalIdePath=[,…] or the LOCAL_IDE_PATH env var (comma-separated). This is - // what makes an OFFLINE verification of the whole declared range possible — download.jetbrains.com is - // not always reachable from every network, and the verifier is the only thing that catches a *binary* - // incompatibility (see InstalledPlugins: `PluginId` is a Kotlin class since 2025.2, so `PluginId.getId` - // compiles fine and then dies with NoSuchFieldError on 242–251). - // When unset/missing (or on CI), fall back to recommended() — which spans the plugin's whole declared - // range including the since-build FLOOR, the gate that catches a too-new API. val localIdes = - ( - providers.gradleProperty("localIdePath").orNull - ?: providers.environmentVariable("LOCAL_IDE_PATH").orNull - )?.split(',') + (providers.gradleProperty("localIdePath").orNull ?: providers.environmentVariable("LOCAL_IDE_PATH").orNull) + ?.split(',') ?.map { it.trim() } ?.filter { it.isNotEmpty() } ?.map { file(it) } ?.filter { it.exists() } .orEmpty() - val offline = localIdes.isNotEmpty() && !providers.environmentVariable("CI").isPresent - if (offline) { - // OFFLINE mode: the given installs are the ENTIRE set to verify against. Neither recommended() - // nor select() may run here — both resolve through download.jetbrains.com, so leaving either in - // made `-PlocalIdePath` a lie: it added local IDEs but still downloaded, and on a network that - // truncates a 1.6 GB transfer the task failed before verifying anything. The flag's whole - // purpose is verification WITHOUT the CDN, so in this mode there is nothing to download. + if (localIdes.isNotEmpty() && !providers.environmentVariable("CI").isPresent) { localIdes.forEach { local(it) } } else { - // THE DECLARED FLOOR, PINNED BY BUILD NUMBER — and this line exists because its absence cost a - // release. The comment below used to claim that `recommended()` covers "the since-build floor"; - // it does not. `recommended()` returns JetBrains' recommended set, which for 253 resolves to - // the LAST point release (253.33813.55), never the first. So the one build the plugin promises - // to support and is most likely to break on — the oldest — was the only one never verified, - // and `com.intellij.modules.jcef` missing from 2025.3 went unnoticed through every green run. - // Keep this in step with `sinceBuild` above: they are the same claim, stated twice, and a gate - // that drifts from the promise it checks is not a gate. - // `useInstaller = false` for the same reason the compile platform above uses it: the CDN has no - // `.tar.gz` named after a build number (`ideaIU-253.28294.334.tar.gz` → 404), only the Maven - // artifact carries one, and pinning the exact build is the entire point of this entry. - create(IntelliJPlatformType.IntellijIdea, "253.29346.138") { + create(IntelliJPlatformType.IntellijIdea, platformBuild) { useInstaller = false } - // Online (CI, or no local installs): recommended() adds JetBrains' recommended spread across - // the declared range, and select() adds the NEWEST EAP/RC. The upper bound matches the declared untilBuild; as of Aug 2026 the newest - // build on either channel is 262.9437.65 (2026.2.1 RC), so this resolves there today and picks - // up a real 263 automatically the day one ships. - // - // BOTH families, not just IDEA. The plugin is used in PyCharm as much as in IDEA, and the - // packaging differences between products are exactly where a classloader problem hides — 5.1.1 - // shipped unusable on 2026.2 because JCEF moved into a bundled plugin there, and verifying one - // product tells you nothing about how another bundles the same platform. recommended() select { - types = - listOf( - IntelliJPlatformType.IntellijIdeaCommunity, - IntelliJPlatformType.PyCharmCommunity, - ) + types = listOf(IntelliJPlatformType.IntellijIdea, IntelliJPlatformType.PyCharm) channels = listOf(ProductRelease.Channel.EAP, ProductRelease.Channel.RC) - sinceBuild = "262" + sinceBuild = "263" untilBuild = "263.*" } } } } } - -kotlin { - jvmToolchain(21) - compilerOptions { - allWarningsAsErrors.set(true) - freeCompilerArgs.add("-Xjvm-default=all") - } -} - -// --- Static analysis and formatting -------------------------------------------------------------------- -// Two tools because they answer different questions, and conflating them is how projects end up arguing -// about braces in code review: Spotless/ktlint decides how the code LOOKS (mechanical, never a judgement -// call), detekt decides whether it is likely WRONG (complexity, swallowed errors, suspicious constructs). -detekt { - buildUponDefaultConfig = true - config.setFrom(files("config/detekt/detekt.yml")) - // Set unconditionally: `detektBaseline` needs the path as an OUTPUT (it is the file it writes), so making - // it conditional on the file already existing makes generating it for the first time impossible. The - // `detekt` task tolerates the file being absent. - baseline = file("config/detekt/baseline.xml") - // Analyse main and test alike. A test that swallows an exception hides a defect just as effectively as - // production code doing it — arguably more so, because it does it while claiming to prove correctness. - source.setFrom(files("src/main/kotlin", "src/test/kotlin")) - parallel = true -} - -tasks.withType().configureEach { - jvmTarget = "21" - reports { - html.required.set(true) - sarif.required.set(true) // consumable by GitHub code scanning if we ever want the findings inline - xml.required.set(false) - txt.required.set(false) - md.required.set(false) - } -} -tasks.withType().configureEach { - jvmTarget = "21" -} - -// --------------------------------------------------------------------------- -// Coverage gates. The INTENT is per package, because one global number would be a lie either way; what the -// tool can actually enforce is a floor plus an aggregate, and the `verify` block below says why. -// -// The honest shape of this codebase is that its risk is NOT evenly distributed. `permission/` decides whether -// the agent may read your SSH key; `ui/` paints a browser. A single global threshold either sets the bar so low -// that the guard could rot unnoticed, or so high that it can only be met by writing tests against Swing and -// JCEF that assert nothing anyone cares about. So the exclusions below say which packages are not gated at -// all, and the rules say what the gated ones must hold — each bound sitting slightly BELOW what is measured, -// so it is a gate that catches regression rather than a target that invites test-padding. -// -// `ui`/`ui.jcef` are excluded rather than gated at a token value. They need a live IDE and a live Chromium, and -// they are covered by a different layer entirely: the vitest suite drives the real shipped JS (`npm test` -// reports how many, and that is the only honest way to say it — a count written here ages on its own, in -// silence, with nobody to notice), and the release checklist requires a manual pass through the UI. Excluding -// them says that out loud; gating them at 20% would dress the same fact up as a passing check. -// -// `context`/`process` are ungated: they wrap the OS (clipboard, process spawn, shell env) and most of what is -// uncovered there cannot run in CI. That is a known gap, not an endorsement. -// -// The measured figures live in ONE place — docs/RELEASE_CHECKLIST.md §Coverage policy — beside the exclusion -// list this block has to agree with. They are deliberately not repeated here: a measurement written into two -// files is a measurement that will disagree with itself, and this file has no way to notice when it does. -// --------------------------------------------------------------------------- -kover { - // Kover aggregates EVERY `Test` task in the project, so a task that is on-demand everywhere else is still - // pulled into the coverage graph and becomes a dependency of `koverXmlReport`/`koverVerify`. Being absent - // from `check` does not keep a task out of this one: it has to be named here. - // - // `checkDrift` is registered as a `Test` task and downloads the latest SDK and probes a LOCALLY INSTALLED - // `claude` binary, which a CI runner does not have. - currentProject { - instrumentation { - disabledForTestTasks.add("checkDrift") - // `uiTest` is the same shape and must be out for two independent reasons. It is a `Test` task - // (registered above), so it lands in the dependency graph of `koverXmlReport`/`koverVerify` — and - // it drives an ALREADY-RUNNING IDE over HTTP, asserting `-PuiTest.enabled=true` rather than - // skipping, so without this line neither report can be produced anywhere that IDE is not already - // up on a display. Its coverage would also be empty either way: the code it exercises runs in that - // other IDE process, which this build never instruments. - disabledForTestTasks.add("uiTest") - } - // The `uiTest` source set is a custom one, so kover reads it as code to measure rather than as tests - // that measure: its RemoteRobot suites showed up as a package `dev.lain.claudejb.ui` at 0% and failed the - // floor. They drive an external IDE and are never code under test. - sources { - excludedSourceSets.add("uiTest") - } - } - reports { - filters { - excludes { - // Need a live IDE / live Chromium to execute at all. Covered instead by the vitest suite, - // which drives the REAL shipped JS (`npm test` is what counts it), and by the manual UI pass - // the release checklist requires. - classes( - "dev.lain.claudejb.view.*", - "dev.lain.claudejb.model.bridge.*", - "dev.lain.claudejb.controller.bridge.*", - "dev.lain.claudejb.controller.commands.*", - "dev.lain.claudejb.controller.context.Link*", - ) - // Thin IDE-action shells: their bodies are one delegate call each, and exercising them means - // booting an IDE to assert that a menu item calls a method. - classes("dev.lain.claudejb.controller.actions.*") - // Wrappers over the OS — system clipboard, process spawn, shell environment. Most of what is - // uncovered here cannot run on a CI box at all. A KNOWN GAP, listed so it is not mistaken for - // coverage; the parts that are pure ARE tested — `ClipboardCli` in `controller/context/`, - // `ImageAttachments` in `model/context/` (ClipboardCliTest, ImageAttachmentsTest) and - // `EnvScriptLoader.parse` in `model/settings/env/`. Those names are load-bearing: a comment - // citing a file that no longer exists is worse than none. - classes( - "dev.lain.claudejb.model.context.*", - "dev.lain.claudejb.controller.context.*", - "dev.lain.claudejb.controller.process.*", - ) - // The Git integration's IDE-bound half: the availability probe (asks the running IDE's plugin - // set), the git4idea gateway (spawns `git log` through the platform) and the hand-off to the - // Version Control tool window. Exercising any of them means a live IDE AND a real repository on - // disk, which is exactly the headless/integration test this package deliberately does not have. - // `GitCommitInfo` — the pure half, and the only place a bug would be silent — is NOT excluded: - // it stays gated and is covered by GitCommitInfoTest. The read-only and API contracts are - // pinned by source/reflection tests instead (GitReadOnlyContractTest, GitApiContractTest). - // - // The trailing `*` is not decoration. `GitGateway.refs()` sorts with - // `compareByDescending {}.thenBy {}`, and each of those compiles to a SYNTHETIC class of its - // own (`GitGateway$refs$$inlined$thenBy$1` and friends) that an exact-name pattern does not - // match. A lambda added inside an excluded object would otherwise start counting against the - // package's floor, which reads as coverage erosion in code that was never gated. - classes( - "dev.lain.claudejb.controller.git.GitAvailability*", - "dev.lain.claudejb.controller.git.GitGateway*", - "dev.lain.claudejb.controller.git.GitHistoryService*", - "dev.lain.claudejb.controller.git.GitLogNavigator*", - ) - // A single line delegating to PluginManager.isPluginInstalled. It exists precisely BECAUSE it - // must run against a real platform (PluginId is a Kotlin class since 2025.2, so the naive call - // dies with NoSuchFieldError below 252) — which is also why a unit test cannot exercise it. - classes("dev.lain.claudejb.util.*") - // The vulnerability view's two platform-bound halves, excluded on the same grounds as - // `process.*` and `ui.*` above and NOT as a blanket on the package: `OsvHttp` is a java.net.http - // wrapper whose every branch needs a live socket, and `VulnService` is a project `@Service` that - // needs a Project, the pooled thread and the EDT. `OsvScanner` is deliberately NOT excluded — - // it talks to OsvHttp through a plain call and its gap is real debt, so it stays gated and - // visible rather than being defined out of the measurement. - classes("dev.lain.claudejb.controller.vuln.OsvHttp*", "dev.lain.claudejb.controller.vuln.VulnService*") - // The IDE MCP servers' platform-bound half: the project `@Service` that owns the sockets and - // the approval notifications, the catalog that binds servers to a Project, and the tools - // themselves (FileDocumentManager, FindInProjectUtil, FilenameIndex — every one needs a live - // index). `ServerEndpoint`, `SocketHome` and `GuardGate` are NOT excluded: they run on a real - // Unix socket and the real guard in unit tests, and the whole `model.mcp` layer is pure. - classes( - "dev.lain.claudejb.controller.mcp.IdeMcpService*", - "dev.lain.claudejb.controller.mcp.IdeToolCatalog*", - "dev.lain.claudejb.controller.mcp.tools.*", - "dev.lain.claudejb.controller.db.*", - ) - // The GitHub plugin's gateway: every call needs the plugin loaded, an account in the IDE's safe and - // GitHub itself — the same grounds as `controller.db.*`. Its availability check and the Marketplace - // gateway stay measured: the first runs headless, the second takes its fetch as a parameter. - classes("dev.lain.claudejb.controller.github.GitHubGateway*") - } - } - verify { - // `KoverVerifyRule` has no per-rule `filters` — re-checked at 0.9.9, the version this build - // resolves, against the plugin's own DSL sources: a rule exposes `groupBy`, `disabled` and its - // bounds, and filters exist on the report set, never on a rule. A report variant is no substitute - // either, because a variant is scoped by source set and not by package. So a threshold per package - // cannot be written. What CAN be written is a FLOOR applied to every package on its own, plus an - // AGGREGATE over all gated code, and the two see different failures: the floor catches one package - // collapsing, the aggregate catches erosion spread too thin for any single package to show it. - // - // Each rule carries a line bound and a branch bound, because they answer different questions. A - // line bound says the code RAN. A branch bound says the decision was taken BOTH ways — and in - // `permission/` and `session/` a branch never taken is a security decision never exercised: the - // guard's deny path, the admission fixpoint's rejection. That code reaches high line coverage - // while never once having said no, which is exactly what a line bound cannot see. - // - // The branch floor is much lower than the line floor because a floor is fixed by the weakest - // package, and on branches the weakest is far below the rest. It is therefore a collapse detector, - // not a regression detector — and the aggregate cannot stand in for it, because a small package - // carries too little of the branch mass to move the total: `permission/` could lose half its - // branch coverage without the aggregate reaching its bound. The floor is the only bound here that - // looks at a package on its own. - // - // Both files must agree, and the measured figures every bound sits below live only in - // docs/RELEASE_CHECKLIST.md §Coverage policy. - rule("every gated package holds its floor") { - groupBy = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.PACKAGE - minBound(65) - minBound(20, coverageUnits = kotlinx.kover.gradle.plugin.dsl.CoverageUnit.BRANCH) - } - rule("gated code as a whole") { - minBound(75) - minBound(40, coverageUnits = kotlinx.kover.gradle.plugin.dsl.CoverageUnit.BRANCH) - } - } - } -} - -spotless { - kotlin { - target("src/**/*.kt") - ktlint("1.8.0").editorConfigOverride( - mapOf( - // The codebase reads at ~120 columns and has done for its whole life; reflowing 13k lines to - // ktlint's default would be a huge diff that buys nothing. - "max_line_length" to "140", - // Trailing commas stay: they are why adding a parameter touches one line instead of two. - "ij_kotlin_allow_trailing_comma" to "true", - "ij_kotlin_allow_trailing_comma_on_call_site" to "true", - // function-signature off. Its only effect here was to COLLAPSE multi-line parameter lists back - // onto one line because they now fit in 140 columns — which trades away the thing the - // multi-line + trailing-comma style buys: adding a parameter is a one-line diff, not a reflow - // of the whole signature. The Kotlin conventions endorse trailing commas for exactly that - // reason and do not require collapsing a signature that happens to fit. - "ktlint_standard_function-signature" to "disabled", - "ktlint_standard_class-signature" to "disabled", - "ktlint_standard_function-expression-body" to "disabled", - // ── One owner per rule ────────────────────────────────────────────────────────────────── - // Below, ktlint duplicates a rule detekt also enforces, and only detekt can scope itself to a - // source set. Running both means the stricter-but-blinder one decides, which is how you end up - // reformatting test fixtures to satisfy a tool that cannot be told they are fixtures. So each - // of these has exactly one owner, and it is the one that can express the exception: - // - // max-line-length → detekt MaxLineLength (excludes the test tree: single-line raw-string - // protocol fixtures, one NDJSON frame each, exactly as the binary emits them). - // function-naming → detekt FunctionNaming (excludes the test tree: test methods are - // backtick-quoted sentences, which is why a failure report reads like a sentence). - // - // Production code is still covered for both — by detekt, at the same strictness as before. - "ktlint_standard_max-line-length" to "disabled", - "ktlint_standard_function-naming" to "disabled", - ), - ) - trimTrailingWhitespace() - endWithNewline() - } - kotlinGradle { - target("*.gradle.kts") - ktlint("1.8.0") - } -} - -/** Extracts the top (latest) `## vX.Y.Z` section of RELEASE_NOTES.md and renders it as the HTML subset - * the Marketplace accepts for change notes. Falls back to a generic line if the file is missing. */ -fun latestReleaseNotesHtml(): String { - val notes = file("RELEASE_NOTES.md") - if (!notes.exists()) return "See RELEASE_NOTES.md." - val lines = notes.readLines() - val start = lines.indexOfFirst { it.startsWith("## v") } - if (start < 0) return "See RELEASE_NOTES.md." - val end = - lines.drop(start + 1).indexOfFirst { it.startsWith("## v") }.let { - if (it < 0) lines.size else start + 1 + it - } - - fun inline(s: String): String = - s - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace(Regex("\\*\\*(.+?)\\*\\*"), "$1") - .replace(Regex("`(.+?)`"), "$1") - - val html = StringBuilder() - var inList = false - - fun closeList() { - if (!inList) return - html.append("") - inList = false - } - - for (raw in lines.subList(start, end)) { - val line = raw.trim() - when { - line.startsWith("## v") -> { - html.append("

").append(inline(line.removePrefix("## ").trim())).append("

") - } - - line == "---" || line.isEmpty() -> { - closeList() - } - - line.startsWith("- ") -> { - if (!inList) { - html.append("") + inList = false + } + + for (line in lines.map { it.trim() }) { + when { + line.startsWith("## v") -> { + html.append("

").append(inline(line.removePrefix("## ").trim())).append("

") + } + + line == "---" || line.isEmpty() -> { + closeList() + } + + line.startsWith("- ") -> { + if (!inList) html.append("