Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,81 @@ This repo is a JetBrains IDE plugin that integrates the OpenCode AI coding assis
provides a tool window, settings panel, and lifecycle management for launching and interacting with the OpenCode CLI
from within the IDE.

## OpenCode 2.x Only

The plugin speaks the OpenCode 2.x HTTP/SSE API exclusively:

- OpenCode 1.x executables are detected at startup and reported with a dedicated "OpenCode 2.x required" message.
Do not reintroduce v1 code paths, endpoints (`/session`, `/tui/*`, `prompt_async`, ...), or dual-version logic.
- All `/api/*` requests carry Basic auth (`opencode:<password>`). Servers the plugin launches receive their
password via the `OPENCODE_PASSWORD` environment variable.
- The embedded TUI is launched as `opencode --server=<url>`. v2 removed the v1 `attach` subcommand, and the
equals form is required: a space-separated `--server <url>` is misparsed as the positional directory argument
(chdir ENOENT, the TUI hangs rendering nothing). `--session=<id>` (also equals form) opens/continues a
specific session.
- v2 API facts were verified against a live v2 server (OpenAPI spec with 119 paths) and the official v2 client
types. When in doubt, trust the live server over memory.
- v2 has **no TUI-control routes** (verified in the v2.0.32 OpenAPI spec — 119 paths, nothing tui-related; the
only prompt route is `POST /api/session/{sessionID}/prompt`). TUI control goes through the embedded terminal
instead: session switching ("New Session", session-list clicks) relaunches the TUI with `--session=<id>`
(the session itself is created via `POST /api/session`, reusing the newest empty one when present), and
"Send Selection" / "Send File" paste a file reference into the prompt input via terminal bracketed paste.
Do not reintroduce the v1 `/tui/*` HTTP routes.

## Prefer JetBrains Tools

When searching the code base or attempting to run a build
Take a look at the available run configurations(`webstorm_get_run_configurations`)
Always prefer `jetbrains_*` tools over
generic alternatives when both are available:

| Task | Prefer | Over |
|-----------------------|-----------------------------------------------------------------------------|----------------------------|
| Read a file | `jetbrains_get_file_text_by_path` | `Read` / `cat` |
| Create a file | `jetbrains_create_new_file` | `Write` / `echo` |
| Find files by name | `jetbrains_find_files_by_name_keyword` | `Glob` / `find` |
| Find files by pattern | `jetbrains_find_files_by_glob` | `Glob` / `find` |
| Search content | `jetbrains_search_in_files_by_text` or `jetbrains_search_in_files_by_regex` | `Grep` / `rg` / `grep` |
| Browse directory | `jetbrains_list_directory_tree` | `ls` / `find` |
| Rename symbols | `jetbrains_rename_refactoring` | text search-replace |
| Check file errors | `jetbrains_get_file_problems` | manual inspection |
| Build project | `jetbrains_build_project` | `./gradlew build` via Bash |
When searching the code base or attempting to run a build, take a look at the available run configurations
(`idea.get_run_configurations`). Always prefer `idea.*` tools over generic alternatives when both are available:

| Task | Prefer | Over |
|-----------------------|---------------------------------------------|----------------------------|
| Read a file | `idea.read_file` | `Read` / `cat` |
| Create a file | `idea.create_new_file` | `Write` / `echo` |
| Find files by pattern | `idea.search_file` (glob) | `Glob` / `find` |
| Search content | `idea.search_text` or `idea.search_regex` | `Grep` / `rg` / `grep` |
| Browse directory | `idea.list_directory_tree` | `ls` / `find` |
| Rename symbols | `idea.rename_refactoring` | text search-replace |
| Check file errors | `idea.get_file_problems` | manual inspection |
| Build project | `idea.build_project` | `./gradlew build` via Bash |

The tools are exposed under the `idea` MCP namespace (catalog paths look like `tools.idea.read_file`). For IDE
capabilities not listed above, use `idea.execute_tool` to invoke the underlying IDE MCP tool dynamically.

## Build

- Build with **JDK 21** (`/usr/lib/jvm/java-21-openjdk-amd64`; project targets JVM 21, Kotlin 2.1.0).
JDK 21 is required: the Kotlin 2.1.0 compiler crashes on JDK 25+ with `IllegalArgumentException: 25...`
from `JavaVersion.parse` ("Internal compiler error") — the release predates those JDKs.
- The crash follows the **Gradle daemon's** JVM (the Kotlin compiler runs inside the daemon). If a
JDK 25 daemon is already running, setting `JAVA_HOME` alone does not fix the build — run
`./gradlew --stop` first, then `export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 && ./gradlew ...`.
- Unit tests: `./gradlew test`.
- Live integration tests live in `src/liveTest` and only run when the `OPENCODE_TEST_VERSIONS` environment
variable lists OpenCode **2.x** versions to test against. They also require `OPENCODE_TEST_AUTH_JSON`
(OpenCode provider `auth.json` content) and, by default, `npm` to install each version. On machines
without node/npm (the dev box has none — OpenCode is a standalone native binary there), set
`OPENCODE_TEST_BIN` to a pre-installed `opencode` binary; its `--version` must match the test list.
Optionally set `OPENCODE_TEST_MODEL=providerID:modelID` to pin the session model (default model
resolution can flake on small local deployments; the model ID must be the provider-registered
one, e.g. `qwen3.8-27b`, not the Ollama-style tag `qwen3.8:27b`).
Live scenarios start with an empty config home, so a provider defined only in the user's own
`~/.config/opencode/opencode.json` (e.g. a local llama.cpp endpoint with a custom model catalog)
is invisible to the test server — turns then fail with `provider.no-route`/deprecated-catalog
errors. Set `OPENCODE_TEST_CONFIG_JSON` to the JSON written as the global config
(`opencode/opencode.json` under the isolated config home; the `providers` definition plus the
default `model` is sufficient, and the API key still comes from `OPENCODE_TEST_AUTH_JSON`).
Live servers are started with a
client-generated `OPENCODE_PASSWORD` (v2 servers always require Basic auth), and sessions are
created with explicit permission rules (v2 removed the v1 `OPENCODE_PERMISSION` env var).
Note: these variables are read inside the **Gradle daemon**, whose environment is fixed at
daemon start — after changing any of them, run `./gradlew --stop` first or the stale value
from the daemon that started stays in effect.

## Kotlin Style

New and changed Kotlin follows the `best-practice-kotlin` skill, adapted to this project:

- Kotlin 2.1.0, JVM 21, 4-space indentation
- No `!!`; sealed types for result/event models (`ApiResult`, `OpenCodeEvent`)
- Read-only collection types (`List`, `Map`, `Set`) at API boundaries
- No new lint tooling (the project does not run ktlint/detekt)
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

## [Unreleased]

<p><strong>Added</strong></p>
<ul>
<li>Support for the OpenCode 2.x server: Basic authentication (<code>OPENCODE_PASSWORD</code>), the durable <code>/api/event</code> stream, session-scoped permission rules, and per-turn diff snapshots.</li>
<li>Automatic detection of OpenCode's background service via its <code>service.json</code> state file, with a live-process check and password read from the same file.</li>
</ul>

<p><strong>Breaking Changes</strong></p>
<ul>
<li>OpenCode 1.x is no longer supported. 1.x executables are detected at startup and the tool window asks for an upgrade to OpenCode 2.x.</li>
</ul>

<p><strong>Changed</strong></p>
<ul>
<li>Server authentication now always uses the fixed <code>opencode</code> user. The settings panel no longer exposes a username or a "Protect server launched by plugin" toggle.</li>
<li>Drop the mDNS launch options (<code>--mdns</code>, <code>--mdns-domain</code>); OpenCode 2.x no longer supports them.</li>
</ul>

<p><strong>Fixed</strong></p>
<ul>
<li>Launch the embedded and external TUI with <code>opencode --server=&lt;url&gt;</code> instead of the removed v1 <code>attach</code> subcommand.</li>
<li>Derive session idle state from execution-finished events, which is what OpenCode 2.x emits.</li>
<li>Request cumulative session diffs with chronologically ordered turn ranges, as required by the v2 diff API.</li>
<li>Restore <em>New Session</em>, session switching (session-list clicks), and <em>Send Selection / Send File to TUI</em>, which relied on the removed v1 <code>/tui/</code> control routes. Sessions are now created via <code>POST /api/session</code> (reusing the newest empty session when present) and the embedded TUI is relaunched against them with <code>--session</code>; references are pasted into the embedded terminal's prompt input.</li>
</ul>

## [3.0.0] - 2026-08-30

<p><strong>Added</strong></p>
Expand Down
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,26 @@ The plugin can launch, monitor, and stop the OpenCode server for you.

- **Start / Stop** — Start the server from the tool window with a single click. The plugin spawns
`opencode serve --port <port>` in the project directory.
- **Auto-detect a running server** — If OpenCode is already running on the configured port (e.g. started from the
terminal), the plugin connects to it automatically without taking ownership of the process.
- **Auto-detect a running server** — If OpenCode's background service is running (detected from its
`service.json` state file and verified against the live process) or a server is already listening on the
configured port (e.g. started from the terminal), the plugin connects to it automatically without taking
ownership of the process.
- **Authentication** — OpenCode 2.x servers require authentication. For servers the plugin launches, it generates
a password and passes it via the `OPENCODE_PASSWORD` environment variable; for detected services it reads the
password from the service state file.

Once the server is running the plugin connects over SSE (Server-Sent Events) to receive real-time events.

### Session list

The **Session changes** panel lists all root sessions. Each row shows:
The **Sessions** panel lists the project's root sessions. Each row shows:

- Session title and a short description
- A spinner while the session is actively running
- The number of files being tracked in that session

Clicking a session row makes it the active session; the file list updates to show only that session's changes.
Clicking a session row makes it the active session — the embedded TUI switches to it (by restarting with
`--session`) and the file list updates to show only that session's changes.

### Modified-files list

Expand Down Expand Up @@ -85,21 +91,29 @@ Share code from the editor directly with OpenCode's prompt input:

- Select any code in the editor
- Use **Send Selection to OpenCode** from the right-click context menu
- The selection is sent as a fenced code block (with filename and line range) to the active TUI session's prompt input
buffer
- The selection is appended to the embedded TUI's prompt input as a file reference
(`@path#Lstart[-end]`) — no file content is embedded, OpenCode resolves the file and line range itself when
you submit

The text is appended — not submitted — so you retain full control before pressing Enter in the TUI.

You can also send file and folder paths to the TUI by right-clicking them in the project view.

These actions paste into the embedded OpenCode terminal, so the OpenCode Relay tool window with the inline
terminal must be open.

### Open in browser

The **Open in Browser** toolbar button opens the OpenCode web UI in your default browser.

### New session in TUI

The **New Session** toolbar button creates a new OpenCode session and immediately switches the active TUI instance to
it.
it. If the most recent session is already empty (no messages, no tracked files), it is reused instead of
accumulating blank sessions.

OpenCode 2.x exposes no non-interactive route for pointing the TUI at a session, so the embedded TUI is restarted
with the new session (`opencode --session=<id>`); the session itself is created via the `POST /api/session` API.

### Permission prompts

Expand All @@ -114,12 +128,14 @@ Responses are sent back to the server immediately.
## Requirements

- A JetBrains IDE based on IntelliJ Platform 2025.3 or later
- [OpenCode CLI](https://opencode.ai/docs) version `1.16.0+` installed and on `PATH` (or the path configured in
settings)
- [OpenCode CLI](https://opencode.ai/docs) version `2.x` installed and on `PATH` (or the path configured in
settings). OpenCode 1.x is not supported — the plugin detects it and explains that OpenCode 2.x is required.

## JetBrains MCP

OpenCode Relay will warn you when JetBrains MCP is disabled or when a running OpenCode server is not connected to it.
The **MCP Servers** action shows each configured server's connection status and lets you connect or disconnect it
from the IDE.

**It is highly recommended to enable JetBrains MCP for the best experience.**

Expand All @@ -130,16 +146,17 @@ To enable JetBrains MCP:
3. Add a `jetbrains` MCP server to your `opencode.json`.
4. Restart the OpenCode server so it reloads the config.

Basic `opencode.json` example:
Basic `opencode.json` example (OpenCode 2.x config format):

```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"jetbrains": {
"type": "remote",
"url": "http://localhost:64342/sse",
"enabled": true
"servers": {
"jetbrains": {
"type": "remote",
"url": "http://localhost:64342/stream"
}
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,16 @@ tasks {
liveTest.runtimeClasspath,
baseTestTask.classpath.filter { it !in testOutputFiles },
)
listOf("OPENCODE_TEST_VERSIONS", "OPENCODE_TEST_AUTH_JSON").forEach { name ->
// Read at daemon configuration time: the Gradle daemon's environment is fixed
// at daemon start, so changing these variables requires `./gradlew --stop`
// before the new values take effect.
listOf(
"OPENCODE_TEST_VERSIONS",
"OPENCODE_TEST_AUTH_JSON",
"OPENCODE_TEST_BIN",
"OPENCODE_TEST_MODEL",
"OPENCODE_TEST_CONFIG_JSON",
).forEach { name ->
System.getenv(name)?.let { value ->
environment(name, value)
}
Expand Down
2 changes: 1 addition & 1 deletion docs/API_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Rules:

- Use shared transport parsers (`parseBooleanResponse`, `parseJsonObjectResponse`, `parseJsonArrayResponse`) for mechanical parsing.
- Apply `withParseContext(endpoint)` at API client boundaries when returning `ApiResult<*>` to callers.
- Define endpoints in domain-grouped endpoint factory files next to each API client (for example `api/session/SessionEndpoints.kt`, `api/tui/TuiEndpoints.kt`) and pass the resulting endpoint object to `withParseContext(endpoint)`.
- Define endpoints in domain-grouped endpoint factory files next to each API client (for example `api/session/SessionEndpoints.kt`, `api/health/HealthEndpoints.kt`) and pass the resulting endpoint object to `withParseContext(endpoint)`.
- `ApiEndpoint` context must remain `METHOD /path` format (for example: `GET /session`, `POST /session/{sessionId}/permissions/{permissionId}`).
- Keep parser error messages generic; endpoint attribution is added by `withParseContext(...)`.
- Do not use `withParseContext(...)` to wrap non-parse concerns (`HttpError`, `NetworkError`).
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Loading