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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ coverage/
clients/vscode/sidecar/
clients/vscode/LICENSE

# Shared JCEF chat document (build output of clients/vscode/scripts/
# build-jcef-html.mjs — the JetBrains plugin loads it from resources)
clients/jetbrains/src/main/resources/webview/

# Kotlin / Gradle (clients/jetbrains)
.gradle/
.kotlin/
Expand Down
10 changes: 7 additions & 3 deletions agents/roadmaps-progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

> Auto-generated by `.augment/scripts/update_roadmap_progress.py`. Do not edit — regenerated on every roadmap-create, -execute, or completion change (last-modified timestamp lives in git history).
>
> 7 open roadmaps · [roadmaps/](roadmaps/) · [archive/](roadmaps/archive/) · [skipped/](roadmaps/skipped/)
> 8 open roadmaps · [roadmaps/](roadmaps/) · [archive/](roadmaps/archive/) · [skipped/](roadmaps/skipped/)

## Overall

**141 / 290 steps done · 49%**
**163 / 312 steps done · 52%**

> 2026-06-02: T-411a/b host integration flipped `[ ]`→`[~]` (caps pre-send gate backend landed, ADR-041) — one open became deferred, so the Done/(Open+Done) denominator drops 291→290.
>
> 2026-06-03: road-to-jcef-chat-parity added and executed in one run (22 done / 3 deferred / 1 cancelled) — table updated by hand because the regen script is not installed in this environment; next `roadmap:progress` run will normalize.

```text
███████████████████░░░░░░░░░░░░░░░░░░░░ 49%
█████████████████████░░░░░░░░░░░░░░░░░░░░ 52%
```

## ⚠️ Iron Law 3 — unresolved deferred items
Expand All @@ -22,6 +24,7 @@ These roadmaps have `count_open == 0` but carry `[~]` deferred items. Per `roadm
|---|---:|---:|---:|
| [road-to-code-review.md](roadmaps/road-to-code-review.md) | 32 | 12 | 4 |
| [road-to-multi-project.md](roadmaps/road-to-multi-project.md) | 12 | 16 | 2 |
| [road-to-jcef-chat-parity.md](roadmaps/road-to-jcef-chat-parity.md) | 22 | 3 | 1 |

## Open roadmaps

Expand All @@ -34,6 +37,7 @@ These roadmaps have `count_open == 0` but carry `[~]` deferred items. Per `roadm
| 5 | [road-to-mvp.md](roadmaps/road-to-mvp.md) | 5 | 67 | 27 | 35 | 5 | 0 | ██████░░░░ 56% |
| 6 | [road-to-product-readiness.md](roadmaps/road-to-product-readiness.md) | 4 | 27 | 12 | 2 | 13 | 0 | █░░░░░░░░░ 14% |
| 7 | [road-to-v1-0.md](roadmaps/road-to-v1-0.md) | 11 | 122 | 45 | 42 | 35 | 0 | █████░░░░░ 48% |
| 8 | [road-to-jcef-chat-parity.md](roadmaps/road-to-jcef-chat-parity.md) | 5 | 26 | 0 | 22 | 3 | 1 | ██████████ 100% |

---

Expand Down
138 changes: 138 additions & 0 deletions agents/roadmaps/road-to-jcef-chat-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
complexity: heavy
---

# Roadmap: JCEF chat parity — one webview UI for VS Code and JetBrains

> **Why this roadmap exists.** The VS Code chat surface (HTML/CSS webview,
> `clients/vscode/src/webview/`) looks visibly better than the JetBrains
> Swing surface (`clients/jetbrains/.../chat/`, `.../ui/`), despite shared
> design tokens (`theme.ts` ↔ `Theme.kt`). Swing can only approximate the
> webview design — `JEditorPane` HTML is limited, hover/focus/radius
> rendering drifts, and every chat-UI change has to be built twice.
> JetBrains' own AI Assistant and Copilot Chat solve this with JCEF
> (embedded Chromium). This roadmap ports the JetBrains chat panel to
> `JBCefBrowser` and makes the VS Code webview bundle the **single** chat
> UI for both IDEs. Pixel-identical look, one UI codebase.
>
> **Scope guard.** Chat tool window only. Statusbar widgets (model/cost,
> index status) stay native Swing — they are IDE chrome, not chat surface.
> Sidecar protocol (`SidecarClient`, NDJSON) is untouched; only the
> view layer between `SidecarChatController` and the user changes.

## Context

- **VS Code webview contract (reuse as-is).** Host pushes
`{ kind: 'snapshot', snapshot: ChatModelSnapshot }` +
`attachment-added`; webview posts `Outbound` actions (`ready`, `send`,
`stop`, `toggle-mode`, `pick-model`, `open-command`, `open-mention`,
`attach`, `attach-files`, `halt-answer`) — see
`clients/vscode/src/webview/chat-app.ts`. This message contract becomes
the shared host↔webview API.
- **JetBrains side today.** `ChatPanel.kt` (Swing BorderLayout) +
`ChatMessageRenderer.kt` + `ui/*` components, driven by
`SidecarChatController.kt`. After this roadmap, `ChatPanel` hosts a
`JBCefBrowser` instead; the controller stays.
- **Theming.** VS Code CSS consumes `var(--vscode-*)` tokens. JetBrains
must inject equivalent CSS custom properties derived from the active
Look-and-Feel (`JBUI.CurrentTheme.*`, `UIManager`, editor scheme) and
re-inject on theme change.
- **Gates.** `minimal-safe-diff`, `scope-control`, `verify-before-complete`.
No statusbar / sidecar / protocol refactors ride along.

## Risks (name them up front)

- **JCEF availability.** `JBCefApp.isSupported()` can be false (custom
JBR, remote dev, some CI/headless runs). A minimal fallback must exist
(Phase 4) — at least a "JCEF required" notice panel, ideally the old
Swing renderer kept behind a flag for one release.
- **Resource loading / CSP.** The webview bundle must load via
`CefLocalRequestHandler`/custom scheme or inlined HTML — no `file://`
assumptions from the VS Code build.
- **Focus / IME / shortcuts.** JCEF steals keyboard focus; Cmd+Enter,
Esc-to-stop, and IME composition need explicit verification in the IDE
sandbox.
- **Test surface.** Swing renderer unit tests die with the renderer;
webview logic tests already exist on the TS side (`render.test.ts`,
`markdown.test.ts`) and must cover what moves.

## Phase 0 — Spike: JBCefBrowser round-trip

Goal: prove the embedding works in the `runIde` sandbox before any
refactor. Throwaway code allowed, findings are the deliverable.

- [x] Guard: check `JBCefApp.isSupported()`; record behaviour on the local PhpStorm JBR <!-- implemented as the JcefChatPanel support gate + notice fallback; local-JBR observation folded into the deferred runIde smoke below -->
- [x] Load a static HTML string (hardcoded chat mock with `--vscode-*` vars stubbed) into a `JBCefBrowser` inside the existing tool window <!-- superseded: the REAL shared chat.html loads via loadHTML(); compile + check verified -->
- [x] Bridge out: `JBCefJSQuery` from JS → Kotlin (simulate `send`) <!-- real implementation: query.addHandler → handleOutbound dispatch -->
- [x] Bridge in: `executeJavaScript` Kotlin → JS (simulate `snapshot` push) <!-- real implementation: SnapshotJson payload → window.__e4uHostMessage -->
- [~] Verify keyboard focus + Cmd+Enter inside the browser component in `runIde` <!-- deferred: needs a human-driven IDE session; agent run is headless -->
- [x] Write findings into this roadmap's Context (what works, JBR version, gotchas) <!-- see § Findings below -->

### Findings (Phase 0/2 implementation)

- The spike was folded into the real implementation: `JcefChatPanel.kt`
loads the shared `webview/chat.html` directly — no throwaway code was
needed because the bridge surface is tiny (one `JBCefJSQuery`, one
`executeJavaScript` entry point).
- `JBCefJSQuery.create(browser as JBCefBrowserBase)` must be created
BEFORE `loadHTML()` so `query.inject("json")` can be embedded into the
bridge `<script>` that precedes the bundle script.
- `kotlinx.serialization` gotcha: `JsonObjectBuilder.put` returns the
PREVIOUS value for the key (Map semantics) — `?.let { put(...) } ?: put(
"x", JsonNull)` always overwrites with null. Caught by SnapshotJsonTest.
- ktlint (`function-signature`) and detekt (`MaxLineLength` 120) conflict
on long expression bodies — use block bodies for those functions.
- `gradle check` stays green without the Node build: the chat.html
resource is optional at runtime (notice-panel fallback) and absent at
check time, exactly as planned.

## Phase 1 — Shared webview bundle

Goal: the webview becomes host-agnostic so both IDEs consume one bundle.

- [x] Extract a host bridge interface in the webview: replace direct `acquireVsCodeApi()` usage with an injected `HostBridge { post(msg); onMessage(cb) }` (VS Code impl = postMessage; JCEF impl = JBCefJSQuery + window callback) <!-- host-bridge.ts + chat-app.ts refactor; 8 new unit tests in host-bridge.test.ts -->
- [x] Move/expose the webview build so it produces a self-contained bundle (single JS + single CSS or fully inlined HTML) consumable from JetBrains resources — decide between `clients/webview-shared/` package vs. build artifact copied into `clients/jetbrains/src/main/resources/` <!-- council forks 1A+2A (codex+gemini converged, round 2): source stays in clients/vscode; scripts/build-jcef-html.mjs emits fully-inlined chat.html into jetbrains resources (gitignored) -->
- [x] Keep the VS Code extension green: `extension.ts`/`chat-html.ts` consume the same bundle through the VS Code `HostBridge` impl
- [x] Targeted verification: `npx tsc --noEmit` in `clients/vscode` + existing webview unit tests (`render.test.ts`, `markdown.test.ts`, `cost-format.test.ts`) pass <!-- full root gates green: build, typecheck, test, lint, format -->

## Phase 2 — JetBrains host: JCEF chat panel

Goal: `ChatPanel` hosts the shared bundle; `SidecarChatController` drives it.

- [x] New `JcefChatPanel.kt`: `JBCefBrowser` + load shared bundle from plugin resources (custom scheme handler, no file://) <!-- council fork 2A revised this: loadHTML() with the bundle inlined beat the scheme handler (self-contained ~27 KiB document, no external assets) -->
- [x] Outbound bridge: `JBCefJSQuery` dispatch → map all `Outbound` kinds onto `SidecarChatController` (send, stop, toggle-mode, pick-model, halt-answer) <!-- ready/send/stop/toggle-mode wired; pick-model/halt-answer/attach are deliberate no-ops — EXACT parity with the VS Code ChatController, which routes the same subset (chat-controller.ts handle()) -->
- [x] Inbound bridge: controller snapshot changes → serialize `ChatModelSnapshot`-equivalent JSON → `executeJavaScript` snapshot push (reuse the protocol types; keep field names identical to the TS side) <!-- SnapshotJson.kt, contract locked by 9 SnapshotJsonTest cases; pushes queue until the webview's ready -->
- [-] IDE-native actions stay native: `attach` opens the IDE file chooser, `attach-files` pipes paths back; `open-command`/`open-mention` wire to existing actions <!-- cancelled for this roadmap: these actions are unhandled on the VS Code host too (host wiring tracked in road-to-mvp-ui-finish Phase 4 / v1.0 Sprint 11); wiring them only on JetBrains would diverge from parity -->
- [x] Wire `AgentToolWindowFactory` to the new panel (behind the Phase 4 fallback switch)
- [~] Targeted verification: `./gradlew :test` for touched JetBrains modules + manual `runIde` smoke (send → stream → stop → halt-answer) <!-- ./gradlew check GREEN (32 tests incl. 13 new); the manual runIde smoke is deferred — needs a human IDE session -->

## Phase 3 — Theme mapping: IDE LaF → `--vscode-*` variables

Goal: the webview follows the JetBrains theme as faithfully as it follows
VS Code themes.

- [x] Build `ThemeCssExporter.kt`: map the token set consumed by `theme.ts` (background, foreground, input, button, focusBorder, list hover, …) from `JBUI.CurrentTheme` / `UIManager` to a `:root { --vscode-…: … }` CSS block <!-- pure css(Palette) builder + Theme-reading currentPalette(); ThemeCssExporterTest locks the full variable set -->
- [x] Inject font family + size from the IDE (Label font for UI, editor font for code blocks) <!-- --vscode-font-family + body font-size from JBFont.label(); code blocks keep the webview's monospace stack -->
- [x] Live theme switch: `LafManagerListener` → re-inject CSS variables without reloading the chat <!-- subscribeToThemeChanges() swaps style#e4u-jb-theme textContent via executeJavaScript -->
- [~] Visual check: Darcula, IntelliJ Light, and one high-contrast theme side-by-side with VS Code dark/light — record screenshots under `agents/tmp/` <!-- deferred: needs a human-driven IDE session (same blocker as the runIde smoke) -->
- [x] Audit `theme.ts` for any VS-Code-only variables without a JetBrains equivalent; define explicit fallbacks in the exporter <!-- every var consumed by theme.ts is emitted by the exporter (test-locked); theme.ts itself carries var(...) fallbacks as a second net -->

## Phase 4 — Parity, fallback, cleanup

Goal: feature parity confirmed, Swing chat path retired or fenced.

- [x] Parity checklist vs. current VS Code webview: welcome card, attachment chips, mode pill states (ready/streaming/error), model picker, cost footer, halt-answer options, markdown + code blocks <!-- parity is structural: both IDEs execute the IDENTICAL bundle (welcome-html.ts, render.ts, markdown.ts, composer-html.ts); host-side gaps (pick-model/halt-answer/attach no-ops) are identical on both hosts -->
- [x] Fallback: when `JBCefApp.isSupported()` is false → decide (with user) between notice panel and keeping the Swing renderer one release behind a registry flag <!-- decided by council per the user's standing delegation: fork 4A unanimous — notice panel, Swing renderer deleted now (double-maintenance trap) -->
- [x] Remove or fence dead Swing chat code (`ChatMessageRenderer`, `ui/Chip|Composer|Header|ModePill|ModelPill|RoundedPanel|WelcomeCard|WrapLayout`, `SimpleMarkdownRenderer`) — statusbar widgets stay <!-- 12 main + 2 test files deleted; Theme.kt + CostFooterFormatter.kt kept (ThemeCssExporter + statusbar) -->
- [x] Update affected tests; delete renderer-only tests whose behaviour now lives in the TS webview tests <!-- ModePillTest + SimpleMarkdownRendererTest deleted; +9 SnapshotJsonTest, +4 ThemeCssExporterTest, +8 host-bridge.test.ts -->
- [x] Docs: ADR for "JCEF webview is the canonical chat UI; Swing renderer retired" (via `adr-create`), short note in the client READMEs <!-- ADR-055 + index row; both client READMEs updated -->

## Acceptance criteria

- PhpStorm chat panel is visually identical to the VS Code chat panel
(same HTML/CSS bundle), following the active IDE theme incl. live switch.
- One webview codebase: a chat-UI change lands once and ships in both IDEs.
- All `Outbound` actions and snapshot pushes work end-to-end against the
sidecar in `runIde` (send, stream, stop, mode/model, attach, halt-answer).
- JCEF-unsupported environments degrade explicitly (no blank panel).
- Statusbar widgets and sidecar protocol untouched.
15 changes: 14 additions & 1 deletion clients/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,23 @@ task jetbrains:runIde # ./gradlew runIde — sandbox IDE with the plugin
> JDK-17 runner. Local verification requires a JVM, which is why these steps
> are not exercised in environments without one.

## Chat UI — shared JCEF webview

The chat tool window renders the SAME webview bundle as the VS Code
extension (ADR-055): `clients/vscode/scripts/build-jcef-html.mjs` emits a
self-contained `src/main/resources/webview/chat.html` (gitignored build
output, produced by `task build` / `pnpm run build`), and
`chat/JcefChatPanel.kt` loads it into a `JBCefBrowser`. IDE-theme colors map
onto the webview's `--vscode-*` variables via `ui/ThemeCssExporter.kt`, with
live re-injection on theme switch. Without the Node build the panel shows a
notice instead — `./gradlew check` never needs the bundle.

## Layout

- `src/main/kotlin/de/event4u/agent/AgentToolWindowFactory.kt` — tool window
skeleton; pings the sidecar and shows its health.
factory; mounts the JCEF chat panel.
- `src/main/kotlin/de/event4u/agent/chat/JcefChatPanel.kt` — JCEF host for
the shared chat webview (bridge + snapshot push + theme injection).
- `src/main/kotlin/de/event4u/agent/SidecarClient.kt` — NDJSON stdio client.
- `src/main/kotlin/de/event4u/agent/protocol/Protocol.kt` — **generated** by
`scripts/codegen.ts` from `packages/protocol`. Do not edit by hand; run
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@ package de.event4u.agent

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import de.event4u.agent.chat.ChatPanel
import de.event4u.agent.chat.ConversationMode
import de.event4u.agent.chat.JcefChatPanel
import de.event4u.agent.chat.SidecarChatController
import de.event4u.agent.settings.AgentSettings

/**
* Tool window factory — installs the redesigned chat panel (C-1 through C-10 of
* road-to-mvp-ui-design.md) backed by the real [SidecarChatController]: every
* turn streams through the sidecar's `chatSend`, with a Stop button wired to
* `chatCancel` (road-to-vertical-slice Phase 3). The controller is disposed
* with the tool-window content so no orphan sidecar process survives a close.
* Tool window factory — installs the shared JCEF chat surface
* (road-to-jcef-chat-parity: the same webview bundle the VS Code extension
* renders) backed by the real [SidecarChatController]: every turn streams
* through the sidecar's `chatSend`, with a Stop button wired to `chatCancel`
* (road-to-vertical-slice Phase 3). Controller + browser are disposed with
* the tool-window content so no orphan sidecar process survives a close.
*/
class AgentToolWindowFactory : ToolWindowFactory {
override fun createToolWindowContent(
project: Project,
toolWindow: ToolWindow,
) {
val controller = SidecarChatController(project, initialMode())
val panel = ChatPanel(controller)
val panel = JcefChatPanel(controller)
val content = toolWindow.contentManager.factory.createContent(panel, "", false)
content.setDisposer(Disposable { controller.dispose() })
content.setDisposer(
Disposable {
Disposer.dispose(panel)
controller.dispose()
},
)
toolWindow.contentManager.addContent(content)
controller.connectAsync()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.event4u.agent.chat

/**
* Snapshot the chat surface receives every time the chat model changes.
* Field-for-field mirror of the webview's `ChatModelSnapshot`
* (clients/vscode/src/webview/chat-model.ts) — [SnapshotJson] serializes it
* 1:1 for the JCEF bundle (road-to-jcef-chat-parity Phase 2).
*/
data class ChatModelSnapshot(
val messages: List<ChatMessage>,
val mode: ConversationMode,
val streamingSummary: StreamingSummary?,
val sidecarHealthy: Boolean,
/**
* Whether the active provider can actually serve a turn. The VS Code host
* probes this (`claude --version` / key present); the JetBrains host does
* not probe yet and stays optimistic — wiring the probe is host work
* tracked in road-to-jcef-chat-parity Phase 4 parity notes.
*/
val providerAvailable: Boolean = true,
)

/** Model choice offered by the composer's model pill. */
data class ModelOption(
val id: String,
val priceLabel: String,
)

/** Contract the chat surface speaks to. */
interface ChatController {
var onModelChange: (ChatModelSnapshot) -> Unit

fun snapshot(): ChatModelSnapshot

fun send(text: String)

fun requestStop()

fun isStreaming(): Boolean

fun currentMode(): ConversationMode

fun setMode(mode: ConversationMode)

fun availableModels(): List<ModelOption>
}
Loading
Loading