This repository implements a modular IntelliJ Platform plugin. It uses content modules as a unit of functionality that the plugin consists of. Content modules are split into:
frontend- UI codebackend- stateful business logicshared
Frontend communicates with the backend through RPC.
This structure allows to:
- separate UI code from business logic
- implement features in a way they work natively in split mode just like in the ordinary monolithic IDE
- keep the plugin code cleaner
The sample plugin adds a ModularPlugin tool window with a chat-style UI implemented with the Swing framework.
A generated project contains the following content structure:
.
├── .run/ Predefined Run/Debug Configurations
├── backend/ Backend module – business logic
│ ├── build.gradle.kts Backend dependencies
│ └── src/main/
│ ├── kotlin/ Kotlin production sources
│ └── resources/ Curly.Embedded.Tools.backend.xml
├── frontend/ Frontend module – UI and presentation
│ ├── build.gradle.kts Frontend dependencies
│ └── src/main/
│ ├── kotlin/ Kotlin production sources
│ └── resources/ Curly.Embedded.Tools.frontend.xml
├── shared/ Shared module – cross-boundary contracts
│ ├── build.gradle.kts Shared dependencies
│ └── src/main/
│ ├── kotlin/ Kotlin production sources
│ └── resources/ Curly.Embedded.Tools.shared.xml
├── gradle/
│ ├── wrapper/ Gradle Wrapper
│ └── libs.versions.toml Version catalog
├── src
│ └── main
│ └── resources/
│ └── META-INF/ Plugin configuration file and logo
├── .gitignore Git ignoring rules
├── build.gradle.kts Root build – assembles the final plugin
├── gradle.properties Gradle configuration properties
├── gradlew *nix Gradle Wrapper script
├── gradlew.bat Windows Gradle Wrapper script
└── settings.gradle.kts Gradle project settings
Note
To use Java in your plugin, create the appropriate /src/main/java directory within the desired module.
The plugin logo is placed in src/main/resources/META-INF/pluginIcon.svg. See Plugin Logo for more information and logo requirements.
root projectassembles the final plugin, declares the main IntelliJ Platform dependency, enables split mode, and includes theshared,frontend, andbackendplugin modules in the final distribution.sharedcontains contracts that both sides must understand: RPC interfaces, DTOs, serializers, and shared model types. Put a cross-boundary API here.frontendcontains UI-only code and presentation logic: the tool window registration, Swing UI, view models, and the frontend adapter that talks to the backend via RPC.backendcontains project-level services and business logic: access to project, file system, and external processes, message creation, response generation, and the RPC implementation exposed to the frontend.
The root build.gradle.kts assembles the final plugin and applies the following Gradle plugins:
| Plugin | Description |
|---|---|
org.jetbrains.kotlin.jvm |
Adds Kotlin support |
org.jetbrains.changelog |
Simplifies patching the CHANGELOG.md file |
org.jetbrains.intellij.platform |
The IntelliJ Platform Gradle Plugin |
The intellijPlatform dependencies block selects the IDE to compile against:
intellijIdea("2025.3.5")See Target Versions for more information.
The intellijPlatform dependencies block also contains a dependency on the platform testing framework:
testFramework(TestFrameworkType.Platform)See Testing for more information
The root plugin.xml file located in src/main/resources/META-INF provides general information about the plugin, its dependencies, and references the per-module plugin descriptors.
Each module ships its own plugin descriptor in its src/main/resources/ directory:
Curly.Embedded.Tools.backend.xml– registers backend extensions and servicesCurly.Embedded.Tools.frontend.xml– registers frontend extensions and tool windowsCurly.Embedded.Tools.shared.xml– registers shared extensions and interfaces
You can read more about plugin configuration files in the Plugin Configuration File section of our documentation.
Generated plugin ID and name may require adjustment.
These values are generated based on Group ID and Artifact ID provided in the IDE Plugin wizard. It is recommended to review <id> and <name> elements in the plugin.xml file, and adjust them if needed.
Please note that Gradle properties rootProject.name and project.group don't need to match the <id> and <name> elements. There is no IntelliJ Platform-related reason they should as they serve different functions.
The demo is intentionally split so that the UI stays frontend-only and the business logic stays backend-only. This ensures optimal UX in the remote development scenario where the IDE has separate frontend and backend processes. This is what we call Split Mode.
A high-level overview of the plugin structure:
- a UI for a chat with an AI assistant natively rendered in the frontend IDE in split mode
- data transfer between the frontend and backend via RPC
- RPC implementation in the backend IDE is capable of touching any backend entities and APIs like a file system
A more detailed explanation of how it is implemented:
- The frontend registers the tool window and creates
ChatViewModel. ChatViewModeldepends on the frontend-facingChatRepositoryApiabstraction instead of directly depending on backend services.FrontendChatRepositoryModelimplements that abstraction by calling the sharedChatRepositoryRpcApiand collecting the backend messageFlow.- The shared module defines
ChatRepositoryRpcApiplus the DTOs used to cross the RPC boundary. - The backend registers
BackendRpcApiProvider, which exposesBackendChatRepositoryRpcApias the RPC implementation. BackendChatRepositoryRpcApiresolves the backend project fromProjectIdand delegates toBackendChatRepositoryModel.BackendChatRepositoryModelowns the mutable message list and the demo response generation logic.
This separation keeps the frontend focused on rendering, local UI state, and interaction handling, while the backend owns project-scoped state and logic that should execute on the backend side in split mode.
Within the default project structure, there is a .run directory provided containing predefined Run/Debug configurations that expose corresponding Gradle tasks:
| Configuration name | Description |
|---|---|
| Run IDE with Plugin (Frontend) | Runs :runIdeFrontend IntelliJ Platform Gradle Plugin task. Use the Debug icon for plugin debugging. |
| Run IDE with Plugin (Backend) | Runs :runIdeBackend IntelliJ Platform Gradle Plugin task. Use the Debug icon for plugin debugging. |
| Run IDE with Plugin (Split Mode) | Runs both Run IDE (Backend) and Run IDE (Frontend) configurations simultaneously to launch the plugin in split mode. |
Note
You can find the logs from the running task in the idea.log tab.
The <idea-version> element is generated by patchPluginXml from
intellijPlatform { pluginConfiguration { ideaVersion { … } } } — editing it in
plugin.xml by hand has no effect, the generated value wins.
<idea-version since-build="261" /> ← 2026.1 and every later release
sinceBuild is pinned at 261 instead of being inherited from the compile target, so bumping
intellijIdea(…) cannot silently drop users on an older branch. untilBuild is set to
provider { null }, which emits no upper bound — that is what lets a build compiled against 2026.1
install into 2026.2 and beyond. Add an upper bound only against a known incompatibility: a stale one
locks users out for a whole release cycle and can only be lifted by publishing a new version.
Compiling against one branch and running on a later one is checked with the JetBrains Plugin Verifier — the same tool Marketplace moderation runs:
./gradlew verifyPlugin # downloads the IDEs listed in pluginVerification.idesNote
A "Not compatible with the version of your running IDE" banner on the Marketplace page usually
means the plugin is still in moderation, not that idea-version is wrong: an unapproved plugin has no
published build, and that banner is how the page renders "nothing installable for you". Confirm with
https://plugins.jetbrains.com/api/plugins/<id> — check approve and hasUnapprovedUpdate — before
changing any metadata. The plugin page is JS-rendered, so fetching the HTML tells you nothing.
signPlugin reads its credentials from the intellijPlatform { signing { … } } block in the root
build.gradle.kts, which accepts two sources — a local PKCS#12 keystore, or three
environment variables for CI. The keystore wins whenever certificate/keystore.p12 is present.
The whole certificate/ directory is gitignored. Pick a password, export it as PRIVATE_KEY_PASSWORD,
and generate a 4096-bit key with a self-signed 365-day chain:
mkdir -p certificate
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:4096
openssl req -new -x509 -days 365 -key private.pem -out certificate/chain.crt \
-subj "/CN=AsulconS/O=AsulconS/emailAddress=adrian.r.bedregal@gmail.com"
openssl pkcs12 -export -inkey private.pem -in certificate/chain.crt \
-name curly-embedded-tools -out certificate/keystore.p12 -passout pass:"$PRIVATE_KEY_PASSWORD"
rm private.pemImportant
Use a PKCS#12 keystore, not the privateKeyFile route shown in Plugin Signing.
Marketplace ZIP Signer 0.1.43 hands encrypted PEM keys to Bouncy Castle without registering its
security provider, so on a stock JDK an openssl genpkey -aes-256-cbc key fails with
Cannot find any provider supporting AES/CBC/PKCS7Padding, and a traditional-format one fails with
PBKDF-OpenSSL SecretKeyFactory not available. privateKeyFile therefore only works with an
unencrypted key. A keystore is read through plain JCA and stays password-protected at rest.
Keep the password ASCII and free of whitespace — it is passed to the signer as a command-line
argument. A stray \r from openssl rand -base64 … | tr -d '\n' surfaces as the misleading
keystore password was incorrect / Password is not ASCII.
Then sign, and verify the result against the chain:
export PRIVATE_KEY_PASSWORD='…'
./gradlew signPlugin
./gradlew verifyPluginSignaturesignPlugin writes build/distributions/Curly-Embedded-Tools-<version>-signed.zip alongside the
unsigned archive; buildPlugin is unaffected. Run the two tasks in separate invocations —
verifyPluginSignature consumes signPlugin's output without declaring the dependency, so requesting
both at once fails Gradle's implicit-dependency validation.
Export the PEM text directly (the values are multi-line; base64-encode them if your secret store cannot hold newlines, and decode before exporting):
| Variable | Contents |
|---|---|
CERTIFICATE_CHAIN |
contents of chain.crt |
PRIVATE_KEY |
contents of an unencrypted private.pem |
PRIVATE_KEY_PASSWORD |
key password — omit for an unencrypted key |
Tip
Make sure to follow all guidelines listed in Publishing a Plugin to follow all recommended and required steps.
Releasing to JetBrains Marketplace uses the publishPlugin task, configured
by the intellijPlatform { publishing { … } } block in the root build.gradle.kts:
export PUBLISH_TOKEN='…' # https://plugins.jetbrains.com/author/me/tokens
export PRIVATE_KEY_PASSWORD='…' # publishPlugin uploads the *signed* archive
./gradlew publishPluginImportant
The first upload of a plugin has to go through the upload form;
publishPlugin only publishes updates to a plugin Marketplace already knows. <id> in
plugin.xml is what ties the two together and cannot change afterwards.
publishPlugin pulls signPlugin into the task graph — a PUBLISH_TOKEN on its own is not enough, the
signing key has to be reachable as well. See Signing the plugin.
The channel is derived from the version's pre-release label, so it tracks version in
gradle.properties instead of being pinned:
version |
Channel | Who sees it |
|---|---|---|
1.0.0 |
default |
every IDE |
1.0.0-SNAPSHOT |
snapshot |
only users who added the channel as a custom repository |
2.1.7-alpha.3 |
alpha |
idem |
At the committed 1.0.0 this publishes to default, visible to every IDE — add a pre-release label to
route a build to an opt-in channel instead. Marketplace refuses a second artifact with the same version, so
recovering from a wrong-channel upload costs a version number: check the table before publishing.
Licensed under the Apache License, Version 2.0.
Portions of this repository — the Gradle build configuration, the split-mode module layout, and the
frontend/backend/shared chat sample — derive from the Apache-2.0 licensed
IntelliJ Platform plugin template,
Copyright 2000-2021 JetBrains s.r.o. See NOTICE for attributions.