All notable changes to this project will be documented in this file. Format follows Keep a Changelog. Versioning follows Semantic Versioning.
Claude becomes one with your IDE. The plugin now runs four MCP servers of its own inside the IDE and hands Claude the IDE itself: 178 tools in 55 domains, on by default, every action mirrored on your screen without taking your focus, every call judged by the guard first. Underneath, the code is restructured, the page is TypeScript, the plugin can be traced, and the bugs found on the way are fixed.
- Four MCP servers of the plugin's own —
code,run,vcs,ops— over Unix sockets, with no port, nothing to install and nothing exposed. Claude reads through the IDE's index (unsaved edits included), searches, navigates by symbol, edits through the document model (one undo entry, a diff, saved), renames and moves with the refactoring engine, reformats with the project's code style, reads the IDE's problems and inspections, builds, runs configurations, runs tests through the IDE's runner, runs commands in the IDE's Terminal, debugs with breakpoints, drives Git through the IDE and its Log, Commit and Pull Requests views, works the Services panel, databases, the HTTP Client and SSH hosts, and can fire any action the IDE registers — with a file, a commit or a Services node as its target. Each server offers its tools on demand, so a session pays only for the domains it uses; results come back as compact tables. - Claude God Mode: one switch, on by default. The flame in the chat bar lights when all four servers
and every rule are on. Each rule is one instruction in Claude's system prompt, repeated every turn,
naming which IDE tool replaces which native one, so a session does not drift back to
grepandsed. Settings ▸ Claude Code ▸ Claude IDE Integration fine-tunes servers, rules, the mirror and whether an unexpected client must be approved. An upgrade that adds domains switches their rules on. - Everything Claude does is mirrored in the IDE, never focused. What it reads opens in the preview tab, what it edits in a real tab; a commit it names is selected in the Log, a service in Services, a problem in its tab, a run in its window; a range it points at flashes. Your caret stays where you are typing and the Terminal keeps its tab. One switch in the same settings group turns the mirror off.
- Claude opens, shows and navigates for you. Ask for a file at a line, a commit, a range of commits, a diff of two files or of a file against any ref, a pull request, a tool window, a Settings page, a Services node, a file in the Project view, a path in the file manager — it appears in the IDE, in the right place, without you touching the mouse.
- Cards for the IDE tools. Every own call is a card named by server, tool and subject; a list passed to a tool draws one sub-card per item, each with its own title, diff, live lines, state and a one-click link into the IDE (the commit, the Log, the tool window, the terminal tab, the run, the problem, the diff). An own edit has View diff and Restore; a long tool keeps its outcome in view; a subagent's calls draw under its Task card.
- Pull requests and releases through the IDE's own GitHub account. List and read pull requests,
open one selected in the IDE's Pull Requests view, create one, comment on it, read its mergeability
and checks until they settle, and merge it once they are green; then verify the tags, the Actions runs,
the GitHub Release and the plugin's versions on the JetBrains Marketplace. No
gh, no token of its own. - Any MCP client can drive the IDE. The servers speak plain MCP over their sockets with the
session's token; a bundled stdio bridge and the protocol are documented in
docs/MCP_CLIENT.md. When the chat page cannot be shown, the servers still start and a notification carries the configuration. - Marks, banners and notifications. Claude can highlight ranges, leave gutter icons and inline hints, ask you something in a banner over the file you are reading, write to the status bar, open a scratch file, set bookmarks and raise a balloon in the IDE's notification area — all gone when the session ends.
- A Log view in the chat's view row. The plugin's own log, filtered by level, with a Copy button that puts a report-ready text on the clipboard. Credentials, prompts and other people's paths never reach it. A Debug switch in the same view turns detailed tracing on for this IDE session.
- The JetBrains MCP Server switch and the third-party Index and Debugger servers are gone. The plugin's own servers replace all three; nothing to install, no port to configure. Custom MCP servers are still yours to add under Custom MCP Servers.
- Update and Plan in the Vulnerabilities view open their own chat tab, named after the job, instead of writing into the chat you were in.
- The plugin uses no deprecated or internal platform API, verified by the Plugin Verifier against
every IDE build from 2025.3.1 to 2026.3 on IntelliJ IDEA and PyCharm, and the policy is written down
in
docs/PLATFORM_API_POLICY.md. - The code is restructured, one responsibility per file. The session orchestrator, the chat bridge, the page host and the guard are split along their seams and every comment is gone. No behaviour changes; the test suite and a new package-dependency gate say so.
- The chat page is written in TypeScript, one small file per concern, compiled into the same scripts the page always loaded. Nothing changes on screen.
- Logging is consistent. One level vocabulary across the plugin:
warnmeans something went wrong,infomarks a lifecycle step,debugis the trace. The binary's stderr and the page's own errors are recorded instead of dropped. - The transcript keeps the last 500 rows on screen instead of 2,000. A long session rendered thousands of rows and the chat slowed down. The model and the page trim at the same number, the notice at the top says how many earlier rows were dropped, and the session file on disk still holds the whole conversation.
- Closing the last chat, or opening the only one, could leave a blank panel — no composer, no tabs, "loading" forever — until Open previous session brought a chat back. The page host gave up on a browser that was still starting and fell back to two delivery routes its own navigation guard refused. The first route now waits for the browser to exist, and the dead routes are gone.
- A page reloaded after a failed delivery kept the loading screen up, because three of the states the host re-sends were remembered as "already sent".
- JavaScript errors in the chat page never reached the IDE log. The page reported them under a message name the host did not parse.
- The model, effort and thinking pills did not survive a new chat, unlike the mode pill next to them: they changed the running session and never the stored setting.
- Fork Session resumed the original session's id, so both chats wrote into one transcript.
/btwnever got its answer. A side question is a full model call; the host gave every control request thirty seconds, declared the question unanswered and dropped the reply when it arrived. A request the binary reports as started now waits for its answer.- Closing a chat while its sign-in was still open left a
claudeprocess running with no tab to stop it. - The Workloads view leaked a pair of mouse listeners on every redraw.
Shift+Tabin the prompt could not leave it, andEscapewith the find bar open was swallowed before the settings menu, the attach tree or the palette saw it.- Switching chats with a search open left the find bar showing a count for a search that was no longer running.
- Every IDE tool call is judged by the guard inside the servers, before it runs, with the same rules as the agent's native tools; a refusal comes back as the tool's error naming the rule and the text that tripped it. The shield, the whitelists and the Security settings page apply to both alike.
- A write through the IDE tools stays inside the project.
write_file,create_file,replace_text,insert_text,move_file,file_from_templateandworktrees addrefuse a destination outside the project root, and the guard judges an own call with its arguments in front of it, exactly as it judges the nativeWrite. The socket files and the parent of their directory are private to the user, like the directory itself. - A path that is only mentioned, does not exist and is not written is a parameter, not a reach, so an API endpoint or a flag that looks like a path no longer trips the outside-the-project rule; a path that exists, or that the command creates or writes, is judged as before.
- A container mount is judged by its host side: the container side of
-v,--mountandkubectl cpis never read as a path on your machine; the host side is. - More privileged-container vectors are refused: further dangerous capabilities,
podmanandnerdctlspellings,kubectlandocsecurity contexts and policies that grant privilege. - The guard's test suite is hardened: assertions pin the verdict a rule must give, not the one the code happened to give, and Windows paths and commands get their own cases across every rule family. The guard's own code is restructured under those tests with no verdict changed.
- The guard sees more of Windows. Writes into the Startup folder and PowerShell profiles are
judged by their content;
copy,move,del,Set-Content,Out-Fileand friends count as file writes with no diff; an 8.3 short name never folds a path inside the project; an explicit NTFS alternate data stream is a write no diff shows; a caret-split command is read as what it runs; and a raw device is named as a device rather than as a network mount. Four Windows false positives are gone: asetassignment binds its variable so a later use is not opaque, aPATHprepend is split on;with a drive letter not mistaken for a separator, a drive-relative path is judged as outside the project, and a local long-path prefix on a project file is spelling, not a network share. - A link written by the model cannot open a file outside the project. Markdown in a reply, a tool
result or an advisory could carry a
jb://openlink to any file under your home; the page now only honours the links the host resolved. And View diff on a pending card refuses a path outside the project instead of reading it.
- Closing the last chat no longer reports an internal error. Closing a chat that was the only one
open, and that had been used, left the IDE logging
Already disposedfrom the browser component the chat had just torn down. The chat's card is now hidden before it leaves the tool window, so the layout never lays out a browser that is already gone. Nothing about closing a chat changes for you: the replacement chat still opens as before.
A chat in the IDE can now be driven from claude.ai or the Claude app, and keeps running on your own machine the whole time.
- Remote Control for a chat, from a phone button in the chat's button row — left of the guard's shield — or from Remote control in the ⚙ menu. Execution and filesystem access stay local: your files, your MCP servers and your project configuration remain the ones in use, and claude.ai or the Claude app becomes a second window onto the same conversation. The transcript announces the session URL when the reply carries one.
- The button reports its own outcome. Lit while Remote Control is on, red with the reason in its tooltip when the request was refused — Remote Control has to be enabled for your account, and on Team and Enterprise plans by an organisation Owner as well. Neither control turns itself on until the binary has said yes.
The guard is now something you can see, tune and audit, instead of a set of rules that only spoke up to refuse something.
- A Guard view in the chat's view row. Every alert raised in this project: what matched, what the rule saw, the verdict, and what let the call through if anything did. Free-text search, multi-select filters by category and by rule, and a Whitelist button on any entry. Retention is configurable; capped at 500.
- The guard keeps its alerts in the IDE's password safe, per project — which is what makes the view above possible, and what puts the guard's rows back when you reopen a chat. Each row returns anchored to the call it judged, and an alert raised inside an agent is drawn in that agent's transcript, not the main one. An Allow All given on a card comes back without its undo link: that approval died with the IDE.
- A shield in the chat's button row, left of auto-scroll: switches the guard to Allow All for a chosen duration, and back with one click. Unlit whenever the guard is not deciding.
- Settings ▸ Claude Code Security, its own page: the guard's mode, a mode per rule with All Enforcing / All Permissive per category, live suspensions you can end, extra credential globs, extra blocked domains, and the whitelist at three reaches — all rules, one category, one rule. Any rule can be whitelisted; credential and foreign-path rules ask for confirmation first.
- A warning row whenever a rule matched and the call ran anyway. It names the rule, what it saw and what let it through, and carries the link that undoes it — including Remove from whitelist, which takes the command off whichever of the three lists is letting it through, narrowest first.
- A Whitelist Command link on a guard block, beside Disable rule. Files the exact command under the rule that refused it, and will not add a duplicate.
- A Vulnerabilities view. Checks your project's dependencies against a public advisory database (OSV.dev) for known CVEs, filters by severity, and hands the findings to Claude to plan how to solve them — reading your code and checking current advisories first, not just bumping a version.
- Export, import and migrate settings, including straight from another JetBrains IDE on this machine. An exported file never carries your environment variables; a keychain-to-keychain migration does, because it never leaves the machine. A permission mode that would weaken security is refused on the way in.
- Both settings pages rebuilt, and they now fit the window. Titled groups instead of one column of forty rows, with Tools, MCP and Advanced folded away; every note sits under its own field and re-wraps as you resize. Nothing runs off the right edge any more.
- The guard has a mode: Enforcing, Permissive or Allow All. Enforcing refuses, Permissive asks on a card every time, Allow All lets the call run. Rules take the first two and are Enforcing by default.
- Settings are per project, per IDE installation. Two repositories can disagree about the model, the permission mode or a security rule. The login stays global, and signing out no longer wipes your settings.
- Always allow this command on a guard alert is per chat, and in memory. It was written to the settings document, so one conversation answered for every other one, for ever. Revocable from that chat's ⚙ menu.
- Every view redraws in place instead of from scratch, so a filter, a scroll position or an open card survives the transcript refreshing underneath it, and an agent's transcript no longer flickers as it runs.
- The branch graph draws to the full height of its row. An
<svg>is a replaced element, so a tall row — uncommitted changes with its file list, a commit carrying several ref tags — had its edge stop short of the next commit and its dot sat below the junction.
- Privilege escalation is refused:
sudo,su,doas,pkexec,runuser,setpriv,run0, the desktop wrappers,osascriptasking for administrator privileges,runas,Start-Process -Verb RunAs,psexec,wsl -u root. Matched only where the payload executes, so a file that documentssudo apt updatetrips nothing. Whitelistable per command. - The "outside the project" rule now sees paths inside shell commands. It only ever read a tool's own
location argument, so
Read /home/you/notes.txtwas refused whilecat ~/notes.txtwas not — and the shell is where the work happens. - Obfuscated payloads are decoded before they are judged — hex and reversed strings.
- Destructive orchestration covers OpenShift:
oc delete projectalongside thekubectlequivalents. - Recovery inhibition covers VSS and APFS snapshots.
- A variable that decides which code runs is never an innocent declaration.
PATH,LD_PRELOAD,BASH_ENV,GIT_SSH_COMMANDand their family are checked wherever they are set. Declaring a path is not reaching it; expanding it is. - System binaries and inert devices are not reaches:
/usr/bin/git statusand2>/dev/nullstill run.
This release needs IntelliJ Platform 2025.3.1 (build 253.29346.138) or newer. On 2026.2 it is the fix: 5.1.1 could not open a chat there at all. On 2025.1, 2025.2 or the first 2025.3, stay on 5.1.1 — it keeps working — or update the IDE.
- A tab per agent, with its own transcript. A second row under the chats lists everything the open chat started — agents, the agents they started, background tasks — all of it at once rather than behind a menu, and scrollable the same way the chats are. Opening one swaps what the conversation area shows; closing it hides a view and destroys nothing, and the card that started the agent opens it again.
- The chat tabs are all one width, so the row reads as a strip rather than as an accordion of long and short titles, and nothing reflows when you select a tab. A long name ellipsises with the whole of it in the tooltip. Selecting a chat centres it, which is what makes ordinary use need no scrolling at all.
- A Chat settings menu on the composer — the wrench beside the prompt box — holding the settings worth changing without leaving the chat, in ten collapsible groups: model, effort, permission mode, the chat toggles, the security lock's rules — 28 of them, behind the nine groups they belong to, so the list is navigable rather than a wall — setting sources, allowed / disallowed / always-allowed tools, and the two MCP switches. Model, effort and mode act on the chat you are in — they are the same controls as the pills beside them, so the two can never disagree — and anything that can only take effect the next time a chat starts says so over its group instead of quietly doing nothing. Everything else is one row away, behind Open Plugin Settings.
- "Always allow" can be granted in advance from that menu, instead of only by answering a permission card for that tool first. What it cannot do is widen the deterministic lock: a credential file, a dangerous command, the system temp folder and anything outside your project still stop and ask, for an always-allowed tool exactly as for any other.
- The composer's two rows of buttons collect what does not fit behind a ⋮, instead of running off the edge of a narrow tool window where nothing could reach them. The send button is never collected and never shrinks to make room.
- Attach ▸ Files… and Directory… browse your project inside the menu, as a tree that unfolds in place,
rather than opening a separate file dialog over the IDE. Pick as many as you like and press Done; marking
a folder marks everything under it and tells you how many that is before you commit to it. It offers what
the IDE considers yours — your
.gitignoreand the project's excluded folders are honoured, sobuild/andnode_modules/are simply not there — and it says so out loud when a folder is too large to offer whole, rather than quietly attaching part of it. - The chat's own buttons are in the chat. New chat, Stop, Commands, Git, Close all diffs and Log out sit in a row above the prompt box, where they are used, instead of in the tool window's title bar — which now holds only the IDE's own controls. Stop is greyed unless a turn is running, and Close all diffs unless there is a diff open.
- Install, sign-in and loading are screens over the transcript, and nothing else. Whichever one a tab owes you covers the conversation and leaves the chat tabs and the prompt box alone, so you can switch chats while one starts and type into a chat whose binary is still coming up — what you type is queued and sent the moment it is ready. A start that takes a moment draws no screen at all.
- Workloads: everything running across every open chat as one diagram, replacing the three lists that were three views of the same tree. Finished work ages out of it on a window you choose in Settings — five minutes through four hours, or All — while anything still running is always shown.
- Background tasks keep their tab and their output after they end, tailed live while they run and rebuilt after a restart. Until now both vanished at exactly the moment the output was worth reading.
- Git, without the plugin ever running
git. A Git button in the chat's own button row opens the repository view, which holds a conversation of its own about the repository — so none of this plumbing lands in the chat you are working in, and none of it makes you leave it either. ⚙ also offers Initialize Git Repository, Commit Changes with Claude and Revert This File with Claude — each one asks Claude to do it, so the command is in front of you in an approval card before it runs and you can answer back ("squash those two", "not that file") instead of getting one shot at a button. On a project that is not a repository yet, opening it offers to create one.- Its turns are always approved by hand — whatever permission mode you are in, and whatever you have marked "Always allow". The plugin started the turn, so it does not inherit permissions you granted for your own work.
- Each entry is hidden where it means nothing: no Initialize where there is already a repository, no Commit with nothing to commit, no Revert unless the file in the editor has actually changed.
- ⚙ ▸ Git Operations — branches and new branch, pull, fetch, push, merge, rebase, stash, unstash and the commit dialog, which are the IDE's own actions: the same dialogs, the same shortcuts, the same enablement, one menu away.
- Git context in the ⚙ menu: the checked-out branch in the menu label itself, your recent commits, and the history of the file you have open — all of it handing off to the IDE's own Git Log. It only ever reads, and on an IDE without the Git plugin, or outside a working copy, the entries are simply absent.
- A Git view in the session dashboard, so the same repository picture and the same actions are one click from the conversation: the branch, what is still uncommitted file by file, and the history as one graph with branch lanes — a commit list and a separate branch map asked you to hold two pictures of the same history at once. Every line and every fork in it comes from real parents and real refs; nothing is inferred, and a line that continues past the oldest commit shown says so rather than ending in mid-air. It reads every branch, remote branch and tag, not only the one you have checked out, because a fork you can only see one side of cannot be drawn. Colour carries nothing on its own: a branch is a text tag on its row and a merge says the word.
- GitHub and GitLab answer for the branch you are on: the pull or merge requests open from it, and its most recent CI run. Read-only, and opt-in — it does nothing until you paste an access token under Settings ▸ Claude Code ▸ Git forge, and until then there is no card and no prompt to configure one. The token is stored in your OS keychain and kept per server, so a company GitLab and gitlab.com are two separate credentials and one can never be sent to the other. Clearing the field revokes it.
- ⚙ ▸ Review This Session's Changes… — everything the agent has touched this session, as native diff tabs, against one base. When the original side cannot be rebuilt exactly the file still opens and that pane says why — new file, binary, too large or restricted, or changed on disk since — because a fabricated original in a review tool is worse than none.
- A Plan view in the dashboard, holding this session's plan in full, with a button that appears only once there is one. It is re-read as soon as you approve a plan, so a revision does not leave the old one on screen.
- The transcript keeps a bounded number of rows in memory, dropping the oldest and saying so in a row at the top. Nothing is lost: the whole conversation is on disk, and "Open Previous Session…" reads it back in full.
- A chat names itself. At the end of the first turn Claude is asked to title the conversation, and the title is kept with the conversation — so it survives a restart and is never asked for twice. Until it arrives the tab shows the first thing you typed, one line, rather than "Chat 3" for the rest of its life. A name you set yourself always wins, whenever you set it. The same name appears in the live tab, in the tabs restored at startup and in the list behind "Open Previous Session…".
- The agent is told what it is running inside — that the transcript is a real interface and not a terminal, that its edits become a diff you review and its paths become links you click, and that a deterministic guard may refuse a call outright. It is fixed text: nothing about your machine, your environment or your project, and nothing that softens a rule.
- Settings moved into the IDE's password safe (the OS keychain), one encrypted document shared by every project. They used to sit in a plain-text file inside the project, committable, including the environment block where an API key ends up. Existing settings are adopted on first run — and because they are now shared rather than per project, the first project you open after upgrading is the one whose settings become everyone's. If you kept deliberately different settings in two projects, note them down first.
- The chat is noticeably lighter. The tab bar and the dashboard used to redraw everything on every update — several times a turn, including updates that changed nothing you could see — and the dashboard did it even while it was closed. Both now redraw only when what they draw has actually changed, so a tab bar no longer rebuilds itself under your pointer.
- The conversation uses the whole width of the tool window. It was capped at a fixed column, so the wider you made the panel the more of it was margin. Diffs, tables and command output are what you get back.
- The dashboard keeps your place. It sits over the conversation instead of replacing it, so coming back from it no longer drops you at the top of a long chat.
- The waiting screens arrive in reading order, and behind a short grace period, so a session that starts quickly draws nothing at all.
- The lines above the prompt box line up. Status, model and working directory, your account, the plan bars and their reset times are five rows on one grid of four equal columns at one size, so the figures sit under each other instead of drifting from row to row. When the panel is too narrow for four, Show more folds away the last two columns of all five rows at once — a bar and its own reset time can never end up separated — and your organisation is left out when it only repeats your email.
- The Workloads diagram's cards are half the width they were, so more of a deep tree fits in a tool window without scrolling sideways.
- Diff History is gone. The per-edit Restore you actually use was never in it — it is on the edit's own card in the transcript, and it stays there. For everything a whole session changed, use ⚙ ▸ Review This Session's Changes…. The composer button that opened it is now the Git one.
- "Roll back all changes" is gone with it, and is not coming back. Without Git it also reverts what you typed between Claude's edits, with no way to tell the two apart; with Git, Local Changes does the same job better and lets you undo the undo.
- A cancelled agent, or one the session limit cut off, stayed on the running animation for ever. Both leave a transcript with no finished turn at the end, which read as work still in flight — and unlike a turn that is genuinely open, nothing more was ever going to be written that could correct it. Both endings are now recognised for what they are and the agent reads as stopped. Measured over the 672 agent transcripts on one machine, 155 of them ended this way.
- Agents stayed "running" for the rest of the session after they had finished, failed or been killed — and with them the Task card in the transcript and their row in Workloads, which take their state from the agent. That last part is why finished work never aged out of the diagram: the window deliberately never hides anything still running. An agent's own record is now read while the session is live, not only when restoring one, so it settles whether or not the binary announces it; and a status this build does not recognise is shown as a failure rather than as work still in progress, because a red row is wrong loudly and a spinning one is wrong in silence.
- "Commit with Claude" wrote its whole turn into the conversation you were in whenever the Git
conversation was not already open — which was most of the time, since nothing opened it on its own. It
always goes to the Git conversation now, which is started the first time you look at the Git view and never
before: it is a second
claudeprocess with its own cost, so nobody pays for it who does not use it. Its answers and its approval cards appear in that view, where the button was, rather than in a tab you would have to go and find. - The Git view kept naming the branch you had left. It now follows the IDE's own Git plugin, so a checkout made anywhere — the IDE, this plugin, a terminal outside it — updates the view.
- ⚙ ▸ Git Operations did nothing. The entries are the IDE's own actions and were being invoked without the context they resolve their target from, so each one quietly decided it was unavailable. They run now, and an action the IDE genuinely refuses says so instead of looking broken.
- Your plan showed as 100% used until you restarted the binary. Once a limit window's reset time passes, the plugin reports it as the 0% it is rather than repeating the number from the window that just ended.
- The command palette covered the box you were typing in, and the taller the composer got the more of it it hid. It sits above the prompt box now instead of floating over it, it is bounded to a readable number of rows, and an unqueried list is in alphabetical order rather than whatever order the commands arrived in.
- Typing a slash command took two presses of Enter — the first completed it, the second sent it. The list is for picking with the mouse; the keyboard enters it only when you press an arrow, and until then Enter sends. Scrolling or clicking the list no longer takes the caret out of the composer either.
- The plugin was dead on 2026.2. The IDE now ships its embedded browser as a separate bundled plugin, and the whole chat UI is that browser, so every chat failed to open. The plugin declares that dependency now, which is also why the minimum IDE moved: it first exists in 2025.3.1.
- Agents showed as failed while they were working, and every agent of every past session came back red after a restart. Their real outcome is read from what the agent itself recorded, so a completed, a resumed and a cut-off agent are told apart instead of all reading as a failure.
- A nested subagent never stopped running. It now finishes with the agent that started it, which it cannot outlive.
- Every agent was also listed as a background task — a second, nameless row whose "output" was pages of the agent's own internal records. Only a real backgrounded command is listed now.
- The tab row could not be scrolled or reached once a few chats were open. It is bounded and the titles are capped (the full one is in the tooltip), the mouse wheel scrolls it, you can grab the row and drag it, and selecting a chat centres it.
- The Chat / Session / Workloads / Git / Plan buttons floated over the transcript and over the tabs, and then disappeared entirely whenever the chat list arrived empty. They are a row of their own directly above the prompt box now, in the same shape as the model and mode pills, so they cover nothing and are always there.
/btwnever showed you an answer. A side question is answered alongside the conversation by a worker of its own, and the transcript deliberately ignores anything that is not the main run — that is the same filter that keeps a subagent's output from interleaving with your chat — so the reply was dropped every time and the question sat there unanswered. It is now asked over the channel that hands the answer straight back, and it appears as a note under your question rather than as a turn. If the binary declines or does not answer, the note says so instead of leaving nothing.- Opening a new chat looked like the plugin reloading. The tab was shown the instant it was created, which meant watching an empty panel assemble the whole interface in front of you. The tab still appears immediately; only the switch to it waits for its page to be ready, and it waits no more than a few seconds so the button can never do nothing.
- A button pressed while your chats were being restored did nothing at all — New chat during startup, and the replacement chat you are owed when you close the last one. Nothing was wired yet, and nothing said so.
- Hovering a tab showed the agents of the chat you were in rather than the agents of the tab under the pointer. The whole row is now visible for the chat you have open, so there is nothing to hover for.
- A restored chat showed the agent's own bookkeeping as things you had said — task notifications, the
"Caveat: the messages below were generated…" preamble, a
/compactyou ran. They are shown for what they are now, and that also stops one of them becoming the chat's title. - The chat was blank under Remote Development. The page now reaches the thin client by more than one route, and if none of them works it tells you which port to forward and the exact command that does it.
- In a resumed or forked chat, a tool call could be filed under an agent it did not belong to, taking everything after it inside that agent as well.
- An access token expiring mid-session asked you to sign in again, when nothing had been signed out. A renewable expiry now says the turn did not complete and to send the message again; only a genuinely missing identity raises the sign-in card. Your message is never re-sent for you, so nothing runs twice.
- The same finished task read green in one view and grey in another. Running, completed, failed and stopped mean one thing everywhere now.
- Closing an agent's tab could kill the chat that started it — the conversation was left on screen over a process that had already been shut down, and it dropped out of the chats restored at the next startup. An agent is a view of its chat, never a second chat, so there is no longer any arrangement in which two tabs share one conversation; the same defect was also what drew a chat twice in the Workloads diagram.
- A block can now be answered where it happens, and the answer expires on its own. A refusal used to be a dead end: the row told you which rule stopped the call, and the only way to act on it was a trip to Settings — where the only choice is to turn that rule off permanently, which is the most dangerous of the options and the one nobody remembers to undo. The block now carries a Disable rule link offering 5 minutes, 15 minutes, 30 minutes, 4 hours, 8 hours, until the IDE closes, or for ever. Five of the seven heal themselves, so the lock spends less time open than it did before this existed, not more. Opening the menu commits to nothing — every entry is the action, so there is no default a reflex click can accept.
- Disabling a rule has never been a bypass, and now nothing implicit can answer for you. A rule you switch
off is downgraded to a question: the same call still stops and puts a card to you, every time, whatever
permission mode you are in. One implicit pass remained and is gone — a tool marked "Always allow" used to
skip that card, which meant a single click on a
Bashcard silently opened every commandBashcan run, including every other one the rule existed to stop. - "Always allow" on a guard card is now about the command, not the tool. Answering it on a
terraform destroycard pre-approvesterraform destroy— that exact command, whole, and nothing adjacent to it: notterraform destroy -auto-approve, not another rule's blocks, not the tool. It lasts only while the rule that stopped it is still open, so re-enabling the rule — or simply letting the suspension run out — revokes it. Anything the guard protects therefore takes a deliberate choice from you, about one command, with the risk knowingly accepted rather than inherited from a setting you made weeks ago. - The release signing keys were rotated, and what vouches for them now travels with the release. The
key that signs the
vX.Y.Ztag and the.ascbeside each download is new, and it is certified by two hardware keys whose private halves have never existed as a file. Everything you need to check that arrives as one attached file,trust-chain.asc— the signing key and both certifying keys together, because a chain is imported whole or it is not imported at all. Verifying isgpg --import trust-chain.ascfollowed by the samegpg --verifyandgit verify-tagas before, and the full procedure is inSECURITY.md. - The single public key that used to sit in the repository is gone, and it is worth being exact about why rather than quietly replacing it: it endorsed nothing a reader could follow — the keys that had certified it no longer exist — and it was not even the key that signed 5.1.1, having been replaced in the tree after that release went out. A key file that verifies nothing is worse than none, because nobody re-checks it and everybody believes it. Each release now carries the chain that was current when it was cut, so a release stays verifiable after the key that signed it has been retired.
Repository and build only — none of it changes the plugin you install.
- The repository is indexed by a generated, gated project map, so a stale map fails the build instead of being believed.
- A reachability gate: code that nothing else references fails the build. What it found on the way in was deleted, this project's signature defect being a feature that is implemented, tested and unreachable.
- The largest files were split by subject — the protocol models, the chat panel, the security guard, the settings page and the stylesheet — with nothing silenced in static analysis to get there.
- The end-to-end UI suite runs nightly rather than on every pull request, it does not block a merge, and it now asserts that it actually executed tests instead of trusting a green build.
- The attribution gate checks that the licence text of every bundled library really travels inside the plugin, which is where the obligation to include it lands.
.gitignoreis an allowlist: it ignores everything and names what belongs, so a new file has to be added deliberately rather than leaking by default.- The published artifact is checked against an allowlist of what it may contain, rather than against a list of things it must not. The repository's own project map was riding inside the plugin jar — 20 KB of internal notes in every release — and banning that one filename would only have caught the file already known about.
- Two more reachability gates, each closing a blind spot the first one declares: one for
privatedeclarations, which only their own file can reach and which the compiler does not report; and one for a page module or stylesheet that exists on disk and is never loaded, which is served to nobody while its own tests pass. - A gate over the tool window's wiring, for a class of defect that leaves no trace: a button is pressed, a nullable lookup resolves to nothing, and there is no error anywhere. It also pins that exactly one place in the code can build a chat panel, which is what makes "one tab per conversation" a property of the code rather than of everyone remembering it.
- The one place this interface knowingly falls short of WCAG 2.2 AA is written down as a decision rather than left as an oversight: the close on the agent row is 20×20 where the criterion asks for 24×24, because the row is 21 pixels tall and a larger control would make the conversation shift every time you opened an agent. On the chats' row, where there is space, it is the full size. It is scoped to that one control and watched by a test in both directions — one that fails if it shrinks further, and one that fails if the reason for it goes away and nobody notices it could be retired.
- The plan limits stopped refreshing whenever the panel was not on screen. The poll was gated on
isShowing, so a collapsed tool window or a chat tab that was not the selected one asked for nothing at all — and a quota window is not the plugin's state to begin with: other sessions, other devices and claude.ai spend the same windows, and a reset is a wall-clock event that owes nothing to this IDE. The figure on screen was therefore whatever the last probe happened to catch, and it only moved again when something else triggered one — a turn, or opening the dashboard. "It only updates when I talk to the agent" is precisely what a visibility-gated poll looks like from outside. The gate is gone and the period is 30 s; what it was saving is one control request per half minute against a process that is already running, and the event-driven refreshes (turn edges,rate_limit_event, dashboard open, session ready) are unchanged.
- The chat's plan-limit row now says how long each window has left —
4h 18mright after that window's percentage, on the window's own line, with the full sentence in the tooltip. A percentage alone does not say whether it is urgent: 90% with eight minutes to go and 90% with six hours to go are different situations, and only the dashboard was answering that. The countdown is computed by one function inapp-core(CC.resetIn/resetInShort) that the dashboard card now shares, and a window with no reset time renders nothing rather than an empty slot that would read as "resets now". The bars row takes three columns instead of the strip's four, which is the one declared exception to the shared column system: the binary reports three windows, so a fourth track is an empty track.
- Every
get_usagepoll now logs the reply it got,rate_limitsverbatim (truncated), at INFO. The derived per-window lines cannot answer the question that keeps coming up — is the number on screen stale, or is the server still saying that? — because a window the reply omits leaves no line at all, and one carried forward from the previous poll is indistinguishable from a fresh one. It immediately earned its place: a live capture showed two of three consecutive polls coming back in the header-seeded shape (five_hour/seven_dayonly, nolimits[],resets_atrounded to.000Z), which is the degraded reply 5.1.0's merge exists for, and confirmed the binary does not cache the endpoint. RateLimitInfo.resetsAtIso()puts the epoch-seconds → ISO-8601 conversion on the model, so a window that reaches a surface from the event stream and one that arrives in theget_usagereport are interchangeable to everything that renders them. It was a private copy in the dashboard's builder, and the composer needed the same thing.
-
An "Other models" group in the model picker, holding previous generations (Opus 4.8 → 4.0, Sonnet 4.6 → 4.0, Sonnet 3.7 and 3.5, Haiku 3.5). Collapsed by default so the four current models keep the menu they had, and expanded automatically when the selected model lives inside it.
The list is curated in the plugin, which deserves stating plainly because this repository removed a hardcoded model label in 4.3.3. There is no runtime source for it: the binary's selectable catalog — the
initializereply, and the identical answer to thelist_modelscontrol request — contains only the current generation, andModelInfocarries nodeprecated/legacyflag. The binary still accepts these ids, it just will not list them. The distinction that makes a curated list defensible here: these are historical ids, which never change and never disappear, so the list can only gain entries. What went stale in 4.3.3 was a label describing the current tier. Nothing here names a current model, and a test enforces that.Choosing a model the account cannot run is handled rather than left to fail:
set_modelis now sent as a correlated control request, and a refusal restores the previous model and says so in the transcript instead of leaving the tab pointed at a model every later turn would fail on. -
Per-model plan limits — Fable among them — are reported.
get_usagereturns them inrate_limits.model_scoped, an array alongside the keyed windows rather than another key inside them (sdk.d.ts:{ display_name, utilization, resets_at }[], and its own example names'Fable').parseUsageReportwalked only the keyed windows, so every per-model figure the server sent was dropped on the floor — which is why the CLI's/usageshowed a Fable row the plugin never did.Reading that array is necessary and not sufficient, which is what the first attempt got wrong: the binary does not relay
model_scoped, it synthesises it, and only behind its own remote config. Its projection (IUt(limits, jJe())in 2.1.223) reads thetengu_usage_overage_included_modelsgate, returns an empty list the moment that gate is empty, and the key is spliced intorate_limitsonly when the projection yielded something — so in a--printsession it simply never arrived, which is why the plugin loggedfive_hourandseven_dayand nothing else while the same account's interactive/usagelisted Fable. The plugin therefore also walks the rawrate_limits.limits[]array the projection reads from, which does ride through untouched — the binary's own/usageformatter assumes as much, callingIUton this very payload — taking theweekly_scopedentries that name a model, with the binary's filter and without its allowlist. Dropping the allowlist is deliberate: it selects which models get overage billing, not which limits a user is subject to, and a limit that meters you is worth showing whether or not you can pay past it.resets_atis epoch seconds there as often as a string, so it is normalised rather than deserialized — a numeric one would have failed to decode and dropped the whole window in silence.And a usage refresh is now merged into the last one instead of replacing it, because the same fetch has a second fallback that omits windows:
loadPlanRateLimitsgives/api/oauth/usage5 s, and on a timeout, a 429 or a fieldless body it substitutesseedUtilization()— an object rebuilt from the rate-limit response headers, which structurally carries onlyfive_hourandseven_day. It is flaggedstatus:"seeded"and then accepted identically to a full reply, so a poll that simply failed was indistinguishable from one saying the per-model window no longer exists — and the Fable bar blinked out and back every few polls. Merged by window key over the whole set, sinceseven_day_opus/seven_day_sonnetare missing from a seeded object for the same reason and would flicker the same way; a carried-forward window keeps the last figure actually reported for it and the next real refresh overwrites it. The extra-credit balance is deliberately not carried:nullthere already means "this plan has none" as often as "this reply did not say".They are keyed
model_scoped:<display_name>because the quota-crossing record is kept per window and has to stay stable across refreshes, and titled from the server's owndisplay_name— the only source for it, since nothing in the plugin can name a window the server invents. An entry whose name collides with a keyed window is dropped rather than duplicated, one missing a name or a figure is skipped, and they sort after the known windows so the row order the user already reads does not shuffle when Anthropic adds a model.
-
The
nimbus_quillusage window is no longer shown. The claude.ai usage endpoint emits it and the CLI relays it untouched; it appears in no version of the binary and in no SDK type, so nothing here can say what it meters — it rendered as "Nimbus quill 0.0%", a row that asks a question and answers none. Hidden by name, deliberately not by a general "hide unknown windows" rule, which would silently swallow the next real limit; the moment it means something, deleting one line brings it back with its label, bar and ordering intact.It kept appearing anyway, because the filter sat on one of the two paths that feed a window to the UI: the
get_usagereport was filtered, therate_limit_eventstream was not, and that is the door it was arriving through. The rule is now applied on both (isHiddenUsageWindow), and on the event path the window is dropped whole rather than merely hidden — it must not become the session'srateLimiteither, which drives the single-number quota bar.
-
A quota notification announcing 100% when almost nothing had been used.
get_usagereports each window as a percentage on a 0–100 scale —sdk.d.tssays so on every window, and a live reply fromclaude2.1.222 carries8and67.ClaudeSessionheld a private copy of an "the wire sends both 0–100 and 0–1, accept either" heuristic that multiplied any value<= 1.0by a hundred. So a window at a genuine 1% was reported as 100%, crossed the 85% threshold, and raised an IDE notification telling the user their plan was spent — at the moment they had spent almost none of it, which is to say right after a window resets. The heuristic is undecidable at exactly 1.0 by construction: it cannot tell a full window from a barely-touched one.The rule now lives once, on the model (
UsageWindow.utilizationPercent()), with no scale guessing: the value is already a percentage. Two of the three copies had been removed in 5.0.1 when the dashboard stopped rounding; this was the third, and the only one wired to notifications, which is why the bars got quieter while the notifications kept shouting. The event path (RateLimitInfo.utilization, genuinely a 0..1 fraction) is unchanged and was never affected.
-
The plan limits are their own row under the status line, one labelled bar per window, instead of dots at the end of the readout. Inline, they sat behind
Running… / Context 65% / 65.3k out / 0 reasoningon a wrapping row — so the windows nearest their cap, the ones the row exists for, were the ones most likely to wrap out of sight in a narrow tool window. The row is arepeat(auto-fit, minmax(150px, 1fr))grid: it spends the full width at any size and drops to fewer columns as the panel narrows, with no media query and no fixed layout to outgrow. The bar is clamped to 100%; the number is not, because a window reported past its cap is exactly the figure worth reading. -
Quota notifications title themselves through
UsageWindow.title(key)rather than from the key, so a per-model window announces "Fable quota at 85%" instead of the syntheticmodel_scoped:Fable. The record that decides whether a threshold has already been announced stays keyed by the key, which is what makes it survive a refresh. -
The
get_usagepath now logs each window's raw utilization and the percentage derived from it, at INFO. When the false 100% was reported there was nothing inidea.logto check it against, because only the event path carried a trace — and that one isdebug, so it is off by default. A number the user can see should leave behind the value that produced it.
-
The subscription login did not survive a restart. The credential was stored correctly — in the IDE's PasswordSafe, which resolves to the OS store — KWallet or GNOME Keyring through the Secret Service on Linux, the Keychain on macOS, the Credential Manager on Windows — and it was still there after the reboot, confirmed by reading the entry back out of the OS store directly. What expired was the access token inside it: the OAuth flow issues one good for hours (~10 h, measured), so any restart the next day found a perfectly persisted credential that no longer authenticated anything.
hasUsableToken()answered false, and false meant "signed out", so the sign-in card came back every morning.The blob beside it always carried a refresh token valid for weeks and the plugin never spent it, by design: only the binary can, and it does so by rewriting
~/.claude/.credentials.json— the exact file the vault exists to remove. The way out is that the binary has a non-interactive login for precisely this: givenCLAUDE_CODE_OAUTH_REFRESH_TOKENandCLAUDE_CODE_OAUTH_SCOPES,claude auth logintakes a dedicated branch, mints a fresh credential and exits — no browser, no TTY, no user. So renewal is now the binary's job, exactly as it always was, and the plugin's job stays what it was: take custody of the result and delete the plaintext copy. No OAuth client here, no token endpoint called from the IDE, no file written back — the invariantNoFileDeletionContractTestand the vault's KDoc both state is untouched.Reported on Linux and Windows, and it is one bug rather than two: the binary's default credential store is its
plaintextprovider (~/.claude/.credentials.json) on every platform, so the vault takes custody the same way everywhere and the token expires the same way everywhere. The fix carries no platform-specific code — the only Windows-specific care is that the renewal environment stripsCLAUDE_CODE_OAUTH_TOKENcase-insensitively, since environment names are case-insensitive there.Scope: the subscription (OAuth) credential only. An Anthropic API key is a different identity in a different slot —
providerApiKey:anthropicin the same PasswordSafe, notCLAUDE_CREDENTIALS_JSON— and it has no expiry and no refresh token, so there was nothing to lose across a restart and there is nothing to renew now.CredentialsVault.renew()reads theclaudeAiOauthblob and nothing else, andenvOverlaywithdraws entirely when an API key is present, so an API-key session is untouched by any of this.An expired-but-renewable credential now counts as an identity (
CredentialsVault.canRenew), the renewal runs off the EDT at launch (ClaudeSession.renewVaultedCredential, before the launch env is built, and never while a sign-in is in flight), the refresh token rotates at every renewal so ordinary use extends it indefinitely, and a failed renewal arms a five-minute cooldown so the three-second boot watcher cannot turn a flaky network into a process spawn per poll. Sign-in is now needed only after a genuinely idle period, or when Anthropic invalidates the grant.
The standards-compliance major. The repository was taken through the standards catalogue domain by domain — application security, licensing, accessibility, supply chain, release engineering, testing and static analysis — and the major number reflects that the code changed to comply, not only the documentation: 108 files, +9 699 / −3 431 lines, of which roughly 4 100 are the JCEF front end.
Compliance here is mechanised rather than asserted. Every claim this release makes is enforced by something that fails a build: detekt and ktlint on Kotlin, ESLint and Prettier on the shipped JavaScript, per-package coverage floors, a distributed-scope dependency audit, CodeQL on both languages, the plugin verifier across the whole supported range with deprecated-API usage as a failure level, and artifact assertions that check the published zip contains no npm code and does carry its third-party notices.
It did not stay purely that, and saying so is cheaper than letting a reader discover it: the release also carries the plan-limits panel and a run of user-facing fixes (below). Nothing is removed or behaves differently on purpose — but a release note claiming "no user-facing change" while shipping a new dashboard card would be the kind of small untruth that makes the rest of the document unusable as evidence.
- The protocol SDK was declared as a runtime dependency while never being one.
@anthropic-ai/claude-agent-sdksat independenciesalthough it is protocol reference material — kept so the Kotlin layer can be diffed against the binary's real surface — and is not executed or packaged. The published artifact contains jars and inlined web assets and zeronode_modulesentries, which anyone can confirm withunzip -l build/distributions/*.zip | grep -c node_modules. The consequence of the wrong declaration was seven permanentnpm auditfindings (three high) against code no user ever receives: an alarm backlog that cannot be acted on, which is worse than no alarm because it trains you to ignore the one that matters. Moved todevDependencies, sonpm audit --omit=dev— the distributed scope — now reports zero.SECURITY.mdstates the triage boundary explicitly, with the command to verify it rather than a request to trust it. - Written threat model (ADR 0002).
SensitiveGuardwas strong and undocumented: nothing said what it defends against, which makes coverage unarguable and restarts every bypass discussion from first principles. The ADR states the trust model (the user trusted; theclaudebinary trusted as software but untrusted as a channel; everything it relays — model output, tool inputs, MCP traffic, file contents, fetched pages — untrusted) and runs STRIDE over the three real surfaces. On indirect prompt injection it records the position deliberately: detection is not attempted, because content-level detection is unsolved and a control built on it would be a liability. Injection is assumed to succeed, and the defence sits where success does not pay — the guard judges the tool call and never the reasoning behind it, so a perfectly-injected model still has to ask to read the key, and still gets the same answer. Non-goals are listed as explicitly as goals. - The ignore rules had no protection for key material.
.gitignorecovered build output and nothing else, so the working tree was one wrong answer away from a committed private key:scripts/bootstrap-ci.shasks where to save a generated JetBrains signing key, and answering.dropsprivate.peminto the repository. It now leads with a secrets section —*.pem,*.key,*.p12,*.jks,chain.crt,passphrase,private.asc,*.token,.npmrc,.netrc— with a single negation fordocs/ci-signing-key.asc, the one key file that must be committed since without the public half nobody can verify a release. Verified by creating each of those files and confirminggit check-ignoreblocks it while the public key stays committable. Secrets come first in the file for a reason: a build artifact committed by accident is noise, whereas a private key committed by accident is burned — forks, clones, forge caches and CI logs mean rewriting history does not un-leak it, and the key has to be rotated regardless. The file itself remains untracked by design (it ignores itself): a published.gitignoreis a public inventory of a maintainer's local directories and tooling, which is reconnaissance for no benefit to anyone installing the plugin. SECURITY.md's supported-versions table still said2.x.
- CI/CD on GitHub Actions, with publication gated three independent ways. The repository had no working pipeline at all: the workflows had been deleted, and a comment in
.gitlab-ci.ymlhad been asserting for months that GitHub Actions was "capped (billing)". That was false — the repository is public, and Actions on standard hosted runners is free and unmetered for public repositories; the account's Actions permissions were verified enabled. A false constraint written into a config file gets believed for years, which is precisely what happened.ci.ymlnow runs the full gate ondevelop,mainand everyfeature/**,bugfix/**andhotfix/**branch — not only on the PR, because a bar you meet only at PR time is a bar you discover late.codeql.ymladds SAST over Kotlin and JavaScript.release.ymlpublishes to the Marketplace only when three things hold at once: avX.Y.Ztag; the tagged commit reachable frommain, asserted before any credential is in scope; and a human approval on themarketplaceenvironment, where the four credentials are scoped and exist for no other job. The middle gate is the load-bearing one — without it, anyone who can push a tag can publish from any code, and the review the approval assumes becomes optional.drift.ymlrunscheckDriftweekly and files an issue rather than committing: whether a new protocol message should be modelled or ignored is a judgement call, and a bot that answers it would bless a gap silently. Every action is pinned by full commit SHA (a tag is mutable, and the action runs with this repository's token), with Dependabot proposing the bumps so the pinning stays free. Build provenance is attested and deliberately not overtrusted — a compromised runner can sign a build that genuinely happened on it. - Release artifacts are signed in the pipeline, by a key that is deliberately not the maintainer's. The maintainer key is hardware-backed and non-exportable — which is what makes it worth trusting, and also why it cannot sign inside a runner. Automating the
.asctherefore needs a software key in a secret, and that weakening is bounded rather than waved through: the secret is scoped to the approval-gatedmarketplaceenvironment (no job reachable from a bare tag push can see it), the key expires after a year so an unnoticed leak stops mattering on its own, and its user ID says out loud that it is a CI key. That last point is the actual mitigation — if the two signatures were indistinguishable, a leaked CI key would impersonate a person. The two claims are now documented as distinct: the tag signature says a person authorised this release, the artifact signature says this workflow produced these bytes, andSECURITY.mdtells users to check both. Generated byscripts/gen-ci-signing-key.sh, which works in a throwaway keyring and never touches the maintainer's. The CI key is certified by the hardware key, which is what makes the arrangement defensible rather than merely documented: without it a user is asked to trust a fingerprint printed in a file inside the very repository an attacker who could swap the key would control — a tautology, not a trust anchor. With it the chain terminates in hardware, and there is a revocation lever nobody holding the leaked key can undo.scripts/bootstrap-ci.shperforms the whole one-time setup, anddocs/CI_SETUP.mddocuments each step for when it has to be done by hand. - Branch protection as versioned code (
.github/rulesets/*.json, applied byscripts/apply-rulesets.sh). Bothmainanddeveloprequire a pull request, an up-to-date branch, signed commits, and every CI check. Required approvals are zero, which reads like a hole and is the opposite: GitHub does not let an author approve their own pull request, so on a single-maintainer repository "require 1 approval" with no bypass actors means nothing can ever be merged — not by push, not by PR, not by admin. We established that empirically, by locking the repository and having to unlock it. The gate that remains is the mechanical one, which is also the one that cannot be talked out of. Raise it to 1 when a second maintainer exists; the rulesets carry that instruction inline. No bypass actors, including admins — the previous documentation preserved an admin bypass for a structural blocker that never existed, and a bypass is by construction used at the worst possible moment on the least-reviewed change..gitlab-ci.ymlis removed rather than retained: two pipelines that can each publish is one publisher too many. - Accessibility conformance work (WCAG 2.2 AA; the EU Accessibility Act has applied since 28 June 2025). A
role="status" aria-live="polite"region declared in the staticshell.html— created lazily it would never announce its first message, which is the classic way to ship a silent live region — plusCC.announcewith duplicate suppression, so a screen-reader user is told when a turn starts, finishes, or is blocked on a permission card. The transcript streams without ever moving focus, so without this the turn simply stalls in silence. Also a:focus-visiblebaseline covering every element whose outline the stylesheet suppresses (the find bar's input had no replacement at all), honoured underforced-colorsrather than overridden. Ten frontend tests pin the structural guarantees; they do not certify conformance, which still requires a keyboard and screen-reader pass by a person. - Third-party attribution ships inside the artifact —
THIRD-PARTY-NOTICES.md,LICENSEandLICENSES/*are packaged underMETA-INF/. The plugin redistributesmarked,DOMPurifyandhighlight.js, and a permissive licence's notice obligation binds on redistribution: a notices file that exists only in the repository does not discharge it for someone who installs the zip. DOMPurify is dualApache-2.0 OR MPL-2.0, so the choice is recorded rather than left implicit. AGENTS.md— the operational runbook for agentic development (commands, gates, boundaries), complementingCLAUDE.md, which stays the architecture.docs/adr/— three decision records: 0001 release process, 0002 threat model, 0003 i18n deferred with the triggers that reopen it.- Conventional Commits enforcement via
commitlintand a versioned.githooks/commit-msg(enable withgit config core.hooksPath .githooks). The hook self-tests and degrades to advisory if its own toolchain fails, specifically so it can never become a reason to reach for--no-verify.
- Published tags are now immutable, recorded in ADR 0001 §3 as a correction of a real violation:
v4.3.2andv4.4.1were each force-re-cut three times after being pushed. A tag is the identity of a shipped artifact; moving one means two people can hold different trees, different zips and different checksums while both believe they have the same version — which defeats the single thing a signature is for. A mistake found after tagging is now fixed by the next patch version. The already-moved tags are left alone, because re-cutting them to "fix" history would repeat the exact mistake. LoginCoordinatorextracted fromClaudeSession(1965 → 1826 lines). The OAuth sign-in is a subsystem in its own right — the TTY-less--printsession cannot host an interactive login, so it happens outside the session entirely through three ordered paths — and it now owns its own state. Mechanical, no behaviour change, full suite green across it. The two further extractions that were considered (SessionRestorer,RewindCoordinator) were deliberately not made:restoreis 23 lines that touch six pieces of session state, and rewind is one of six identically-shaped control-request delegates. Both would have bought indirection rather than cohesion, and saying so is the point of recording it.package.jsondeclared"license": "ISC"on a GPL-3.0-only repository and lacked"private": true— i.e. it was publishable to npm under the wrong licence. Corrected.- No contact email is published anywhere in the project. The
<vendor email>attribute is optional and has been dropped fromplugin.xml; vulnerability reports now go through GitHub private security advisories rather than an inbox. That is the better channel on its own merits and not only a privacy measure: the report lands in a private thread attached to the repository, the discussion and fix stay linked to it, and a CVE can be requested from the same advisory — whereas an address in a public file is scraped far more often than it is used by a reporter. - Protocol baseline re-verified and advanced to
claude2.1.222 / SDK 0.3.222;./gradlew checkDriftgreen, protocol surface unchanged. - The pull-request template now asks for risk and rollback — and for a published plugin, reverting a commit is not a rollback: a user on the bad version stays there until they update.
- The frontend test harness now extracts the shell DOM from the real page instead of a hand-copied approximation. The copy had already drifted — it lacked
#a11y-status— which is the worst failure mode a harness has: it does not fail loudly, it quietly tests something that is not the product. - Frontend suite: 44 → 54 tests. JVM suite 677 → 682.
- detekt and Spotless/ktlint added, and the 203 findings they raised were fixed rather than frozen. Until now the entire quality bar for 13k lines of Kotlin rested on review, which is precisely what the standards say to mechanise. The first run produced 492 findings; tuning the rules with the reasoning written at each setting brought it to 203, and those were then worked down to 2.
config/detekt/baseline.xmlholds exactly those two, both aboutClaudeSession, both explained inside the file — it is a record of a decision, not a drawer. The distinction matters: a 203-entry baseline is a promise to nobody, a 2-entry one is a claim somebody has to defend in review. - The dispatch tables were split in two levels, keeping compile-time exhaustiveness.
ClaudeSession.onEventwas a singlewhenover 47 event types — 244 lines, cyclomatic complexity 111 — the one function where every protocol concern in the plugin met.ClaudeEventnow declares seven sealed sub-interfaces (Stream,Conversation,Control,Task,Notice,SessionSignal,HookTelemetry) and dispatch picks the group, then the variant. The grouping is expressed in the type on purpose: a sealed hierarchy keeps the compiler checking exhaustiveness at both levels, so a new protocol event that nobody handles is a compile error rather than a silently dropped frame — which is the propertycheckDriftexists to protect, and was not up for trade against a complexity threshold. The groups are semantic, not cosmetic: they differ in what the host owes the binary (aControlframe must be answered or the binary hangs; aNoticeis fire-and-forget).JcefBridge.MsgandJcefChatPanel.onBridgeMessage(complexity 46) got the same treatment, with the message groups mirroring the bridge's parsers one-for-one. - Several
whenchains were dictionaries written as control flow, and are now data:ProtocolParser.parseSystemhad 25 arms of which 21 were the same expression with two names substituted (complexity 29 → aMap), likewise the top-level frame decoder, andEditorContextProvider.langForExtension(26 arms → a lookup table). Adding a protocol subtype is now one line, and the shared fallback wiring is written once instead of 21 times where a mistyped argument would have been invisible. - Coverage is gated per package (
koverVerify), because risk here is not evenly spread:permission/decides whether the agent may read your SSH key,ui/paints a browser. Thresholds sit slightly below what each package measures, so they catch regression instead of inviting test-padding.ui/,context/,process/,actions/andutil/are excluded with the reason stated rather than gated at a token value — gating them at 20% would dress the same fact up as a passing check. Policy, measured numbers and the known gaps are indocs/RELEASE_CHECKLIST.md§Coverage policy. - A "≥90% coverage target" was cited in the build for a requirement that did not exist.
build.gradle.ktsclaimed the figure was "documented indocs/RELEASE_CHECKLIST.md"; that file had never mentioned coverage, and the real number was 53.3%. A number nobody measured, pointing at a rule nobody wrote. - ESLint and Prettier now cover the shipped JCEF frontend — ~3.6k lines of JavaScript that ride inside the plugin jar and had never passed through any tool.
no-eval,no-implied-evalandno-new-funcare errors because the page runs under a hash-pinned CSP with no'unsafe-eval': without the gate, code Chromium will silently refuse in a user's IDE can still reachmain. Vendoredmarked/DOMPurify/highlight.jsare excluded — a finding in them is not ours to fix, and fixing it would fork a dependency. - A
Static analysisjob (detekt,spotlessCheck,koverVerify,npm run lint,npm run format:check) is now a required check on both protected branches. Everything above is only worth having if breaking it fails a merge. - Two rules that both tools enforced were given a single owner each:
max-line-lengthandfunction-namingare detekt's, because only detekt can scope an exception to the test tree. Running both meant the stricter-but-blinder tool decided, which is how you end up reformatting single-line NDJSON protocol fixtures to satisfy a tool that cannot be told they are fixtures.
- Token counts and CSS alpha values were formatted with the machine's locale.
TokenFormat.trimDecimalused the default-locale"%.1f", so on a comma-decimal machine (Spanish, German, French…) a count rendered as1,2kinside otherwise-English UI — and worse, the trailing-.0test stopped matching, so a flat 1000 tokens displayed as1,0kinstead of1k. The same bug inJcefTheme.rgbawas not cosmetic at all: it emittedrgba(217, 119, 87, 0,140)— four components instead of three — so the browser discarded the declaration and the--accent-soft/--link-softwashes (text selection, the code-block Copy hover, the "View diff" hover, blockquote backgrounds) never rendered on those machines. Also fixed in the context-usage percentage and the colour-to-hex helper. All now pinLocale.ROOT. - Diff tabs were being persisted into the workspace and could never be restored. Our diffs are in-memory previews (
ChainDiffVirtualFileover amock:///URL); the platform persists every open editor tab by URL without filtering by file system, so on the next start each one resolved to nothing. One workspace here had accumulated 13 such entries — all namedClaude · SKILL.md, since the tab title is the file name and a skills repository has oneSKILL.mdper directory — producing 26WARN EditorsSplitters - No file existslines on every single launch.DiffTabCleanupnow closes them onprojectClosingBeforeSave, the one hook that runs before the state is written (projectClosingwould be one step too late), and a wiring test pins theplugin.xmlregistration against the shipped descriptor — the failure mode being silence, not a stack trace. CloseAllDiffsActionmoved to a background update thread. It reads oneCopyOnWriteArraySet's size; keeping it on the EDT put it in the queue behind everything the IDE does at startup.InterruptActiondeliberately stays on the EDT and now says so in the code: it readsContentManagerImpl.mySelection, anArrayListmutated on the EDT with no synchronisation and no threading assertion, so moving it would trade a cosmetic log line for a rareIndexOutOfBoundsException.- Four defects in the shipped frontend, all found by its first lint run:
obj.hasOwnProperty(k)in both DOM-building helpers (breaks if the object carries its ownhasOwnProperty— and those helpers build DOM from host-supplied data), an emptycatchin the Vibe Mode theme restore that silently left the theme half-reverted, and two dead functions (isAgentTool,esc) nobody called. sniffMediaTypeno longer confuses any RIFF container for WEBP. Rewritten around named signatures (complexity 23 → 4), it now checks the four-byte form type that actually identifies the format, not just theRIFFheader that WAV and AVI share.
- No chat could be opened or restored (regression, introduced on this branch).
JcefChatPanel.pendingUntilReadywas declared below theinitblock that uses it. Kotlin runs property initializers andinitblocks in declaration order, so the list was stillnullwhileinitran and the constructor threwNullPointerException— taking the whole tab with it, on new chats and on startup restore alike.lastUsage/lastUsageAthad the identical defect and stayed silent, because a nullable reference and a primitive read asnull/0instead of throwing: the loud version of this bug was the lucky one. The compiler does not catch it — it flags a direct reference in an initializer, but here the read happens inside a function called frominit, which it cannot see through — soInitOrderContractTestscans the sources and fails the build on any class-body property declared after its owninit. - Nothing said the agent was still starting. The binary is now launched before the tab is built (
start()only dispatches, soclaudeboots while JCEF creates its browser), and a boot screen holds the tab until the process is up. Three states, not two:running,starting, and neither — that last one is a launch that failed (missing binary, declined trust prompt, refused remote-mount project) and it must bring the screen down, or the tab stays covered forever with no way to reach the notification explaining why. The screen is declared visible in the static shell, since at page load the process genuinely is not up yet. - Context and cost were a minute late, twice over. A
javax.swing.Timer's initial delay equals its interval, so the first poll came a fullQUOTA_POLL_MSafter the panel attached — and that tick landed while the binary was still launching and returned early on the not-running guard, costing a second interval. Process-ready, tab-open and both turn edges now poll directly, and the timer retires at the end of a turn: context and cost cannot move while a session sits idle, so polling forever was a round-trip through the binary, per tab, for two numbers that provably had not changed. - The plan-limit figures disagreed with themselves. A
get_usagereply refreshed the dashboard bars but not the composer's dots, so the same number appeared immediately in one surface and "a while later" in the other, whenever some unrelated state change happened to re-push. Both are pushed together now. Opening the dashboard also refreshes them, whichrequestUsage's own contract had claimed and the code had never done. - Reasoning tokens, context and output are rendered at
0instead of omitted. An item that only appears once it is non-zero is indistinguishable from one that failed to load — which is exactly how a fresh tab read: a lone "Idle" and no figures. Cost stays gated, because a currency amount of zero is noise rather than an ambiguity to resolve. - The CLI's
<tool_use_error>wrapper reached the transcript verbatim.claude2.1.222 wraps a failed tool result'scontentin that tag pair and carries the same message unwrapped in a sibling field — framing for the model, not text for a human. Rendered as-is it put raw markup in a native GUI, the "never mirror raw CLI output" antipattern this plugin exists to avoid. Stripped only when it encloses the whole payload, so output that legitimately mentions the tag survives;is_erroralready conveys the failure structurally, and is what reddens the card. - A failed tool card hid its own error. Tool output lives behind the card's collapse, so for a failure the entire message was "the header is red", and the text scrolled sideways rather than wrapping — hiding the actionable half at the end of the line. A failed card now opens itself once (tracked on the node, so it never fights a user who deliberately collapsed it) and its error text wraps. Healthy output still scrolls: wrapping code or a log corrupts its alignment.
ToolSearchwas missing from theSensitiveGuardtrust allowlist, along withAskUserQuestion,McpandFileRead/FileEdit/FileWrite.ToolSearchis the one that mattered: it loads the schema of every deferred tool, so on a session that defers them, the call that unlocks all the others was the one landing in the third-party branch. Entries are only ever added to that list — it is a trust allowlist, not an inventory, and a missing first-party name is precisely the 4.4.0 hard-DENY incident. Found by diffing it against a live session's real tool inventory rather than against the SDK's type names, which are not the runtime registry (the SDK calls themFileRead/FileEdit/FileWrite; the tools areRead/Edit/Write).- A Markdown link whose href is a path did nothing when clicked. The host handled
https://andjb://openand dropped everything else without a sound — so[BACKLOG](docs/BACKLOG.md)was inert while bare paths written in prose worked, making the more deliberate link the one that failed. Both routes now go through a single authorising gate (LinkResolver.isOpenable). The scheme test requires two or more characters before the colon, so a Windows drive (C:\src\main.kt) stays a path rather than being mistaken for a URI scheme. - Copy on a message copied and said nothing. Message-level buttons carry their own click handler and never reached the delegated code-block path that flashes "Copied", which reads as a broken button — and was reported as one. The flash helper is now exported and shared rather than reimplemented, so wording and duration cannot drift. The
.copiedclass had been applied by the JS since 4.0.4 and had no CSS rule at all; it now has one.
-
Onboarding: the plugin now installs and signs in Claude Code from inside the IDE. A tab opened without the
claudebinary shows an install card instead of a loading screen that faded into an empty tab: one button per official install route for the current OS (Linux: install script, plus apt/dnf/apk when the distro is recognised; macOS: script and Homebrew; Windows: PowerShell, winget and cmd), each with the exact command it runs shown beside it, copyable — on a network that blocks one route, the command itself is the fallback. Commands run visibly in the IDE terminal; a manual path entry accepts a file or an install directory and is validated by running--versionand requiring the answer to name Claude Code. Once the binary appears — by any route — the session starts on its own. -
Sign-in lives in a card, not a command — and the credential does not live on your disk. Signed out, the card is the first thing a tab shows, before a turn can fail on it. The subscription flow is fully native and requests the full OAuth consent — the reduced
setup-tokengrant drops scopes Claude Code exercises, file upload among them, which is what a pasted attachment travels on: the binary's browser flow runs under a hidden PTY, the card shows the URL (copyable) and completes on its own when the browser finishes, so pasting the code is an optional fallback on the same screen rather than a step of its own. That login normally leaves its credentials in~/.claude/.credentials.json— plaintext on Linux, readable by every process running as you, and shared with the terminal CLI. The plugin does not leave them there: they are moved into the IDE's password safe (OS keychain / KWallet / DPAPI) and the file is overwritten and deleted, a login made in your own terminal included. Nothing ever writes that file back. The credential reaches the binary through the process environment instead, which is narrower (/proc/<pid>/environis owner-only where the file was readable by anything running as you) and leaves nothing behind when the process exits. An orphan left by a hard IDE kill is folded back into the safe at the next launch.The binary keeps running against your own
~/.claude, untouched, and it is handed the whole credential through the environment — access token, refresh token, OAuth scopes, subscription type, rate-limit tier and the account — not just the token. The scopes are the load-bearing part: the plan-limit windows come from an endpoint the binary only calls when the credential grantsuser:profile, so handing over a bare token left every session meter dark. That was misread during development as "the binary only reports this from its own configuration directory", and the fix attempted from that premise — a private configuration directory with your real configuration symlinked into it — deleted the contents of the directories it linked to when the session ended, session history included. That directory is gone, along with the recursive delete at its heart; the plugin now deletes exactly one file, ever: the plaintext credential it moves into the safe. A source-level contract test fails the build if any other deletion appears.An API key entered in the card goes to the same per-provider slot Settings uses, so the card and Settings ▸ Provider are one credential rather than two that disagree — and no provider's key can overwrite another's. A valid key that the binary rejected is fixed too: it requires each key to be approved once, and a
--printsession has nobody to ask, so the approval is recorded when you enter the key, and the key is verified before being stored at all.claude auth statusvalidates whichever identity is effective and enriches the dashboard's account card, whose row always shows Sign in or Log out — and Log out stops the session first, then clears the IDE's copies, without touching your own terminal login./loginis no longer advertised in the palette — typed, it still works.Sign-in comes before the loading screen: verifying credentials needs no session, so an unauthenticated tab shows the card rather than launching a process to discover what it already knew. And all of it is re-checked continuously — installing the binary or signing in from elsewhere takes effect within seconds, with no tab to close and reopen.
-
Plan-limit bars in the session dashboard and a matching dot in the composer readout: every rate-limit window plus the extra-credit balance, colour-graded by severity, animating to their value so the number and the bar settle together. Each source is read on the scale it actually uses — the live events carry a
0..1fraction, the on-demand usage reply0..100. -
The chat is reachable only while Claude Code is running. Install → sign in → loading → chat, and any step backwards — the binary uninstalled, the credential gone, the process exited — stops the session and returns to the matching screen. The loading screen waits for the binary to answer rather than merely to start, so the first frame is drawn with the command list, model catalogue and account already in hand.
-
A unified entrance for every transcript row. Messages, tool cards, thinking folds, recalled-memory folds, elicitation cards and notices all rise into place on the same curve; a completed tool call resolves with a single 1.5% beat, sized to register at the edge of vision rather than to be watched.
-
A boot overlay covering the interval between launching the binary and the session being ready, with a distinct state for a launch that failed.
-
Reduced motion is driven by the IDE, not by the browser's own media query, so it follows the setting the user actually changed. Its handling is explicit rather than a blanket freeze: looping indicators keep a legible resting state instead of stopping on their first frame.
-
Failed tool output wraps instead of scrolling sideways; Copy affordances share one confirmation state; the loading indicator and empty state use the same Claude glyph, drawn as a character rather than an asset so the hash-pinned CSP is unaffected.
- Release tags are cryptographically verifiable. The CI signing key carries an email identity, is registered on the publishing account, and is certified by the maintainer's hardware key; the workflow derives the tagger address from the key it signs with, so key rotation is self-contained and the two cannot fall out of step. The address is never written into a committed file. A key without an email identity now aborts the release.
- The tag precedes the artifact.
publishcuts and signs the tag, checks it out, and builds from that ref, so the published bytes correspond to the ref that names them. Re-running the job on an existing tag is idempotent and replaces the assets in place, which makes recovery from a failed publish a re-run rather than a manual intervention against an immutable tag. - Publication is a single reviewed act. Merging the release pull request into
mainpublishes; credentials remain scoped to themarketplaceenvironment and unreachable from any other job.scripts/bootstrap-ci.shprovisions the environment, both deployment refs, all six secrets, the signing key and its account registration in one idempotent run. - The GitHub Release carries
CHANGELOG.md; the Marketplace "What's New" panel continues to carryRELEASE_NOTES.md. An empty extraction fails the release.
- Segmented CI images:
node-test(462 MB) for the npm jobs,jvm-test(8.08 GB) for the Gradle jobs, with the artifact-assertion and release-readiness jobs on bare runners. Container startup for the frontend suite is 10 s, from 5m37s. The plugin verifier's IDEs are resolved at run time rather than baked, keeping them current with the EAP/RC channels they come from. - The Gradle cache in the image is genuinely warm: the IntelliJ Platform is extracted at image-build time and the build is verified to compile offline from it. A warm-up failure fails the image build.
- CodeQL is a required check on
developas well asmain, with thejava-kotlinanalysis running on the same JDK and warm cache as the rest of the pipeline. - A release-readiness gate blocks
develop → mainwhile an automated pull request is open againstdevelop, so a release cannot ship alongside an unmerged dependency update. - Branch protection, deployment policy and required checks are versioned and applied by script.
- A one-time Marketplace review prompt (
ui/ReviewPrompt). After 25 successful turns — errors and interrupts don't count — a single, non-modal IDE balloon asks for a review, then never appears again for that installation. Deliberately conservative: the counter and the "asked" flag live in application-levelPropertiesComponent(so a second project neither resets progress nor produces a second prompt), the flag is written before the balloon is shown (a crash can't cause a double ask), and there is intentionally no "remind me later" — a deferral is a nag with extra steps. Rationale, measured on the live MarketplaceClaude Codequery: rating outweighs download count in ranking (a plugin with 8.6k downloads and a 4.54 rating outranks several with 4–10× the downloads, while 4.4M downloads at a 2.38 rating rank below them), so a bad rating is worse than none — which is why the prompt is built to be impossible to experience as nagging. The policy (shouldAsk/recordTurn) is pure and unit-tested, including that it fires exactly once when counting past the threshold.
- GitHub repository metadata for discoverability — the repo had no topics and no homepage. Added 20 topics (
claude-code,anthropic,jetbrains-plugin,ai-agent,intellij-plugin, the IDE names…), pointed the homepage at the Marketplace listing, and rewrote the repo description with the terms people actually search for.
-
A reloaded transcript rendered command cards in the pre-4.3.2 style — no code block, nothing to copy. Restoring a past session reconstructs the transcript through
SessionTranscriptReader, a code path entirely separate from a live turn, and it never populated the two inputs the current rendering needs:commandTexton theTOOLrow (so no copyable command block) and thecommandtag on theTOOL_OUTPUTrow (so the output fell back to plain text). Both are now derived exactly as the live path derives them —SensitiveGuard.commandTextfor the call, and a post-parse pass (tagCommandOutputs) for the output, needed because the JSONL emits atool_resultin a later message than itstool_use, so at parse time the output's own line carries nothing identifying it as a command's. Theis_errorflag is read too, reproducing the live"command error"tag set. This is the second instance of the same class of bug — 4.3.1 fixed restored file cards losing their jump-to-code path — so the restore path now has regression tests pinning that it produces the same row a live turn does. -
/loginalways dead-ended on "run this yourself in a terminal". Every platform APITerminalLauncherreflected on to open the terminal tab was missing at runtime on a current IDE: the Reworked path looked upcom.intellij.terminal.frontend.toolwindow.TerminalToolWindowTabsManager, which is not present in the shipped IDE at all (verified by scanning every jar of IU-262.8665.337), and the Classic path calledTerminalToolWindowManager.createShellWidget(…)/.createLocalShellWidget(…), both of which existed on 251/252 but were removed by 262. Each lookup returnsfalserather than throwing, so the failure was completely silent — nothing reached the log, the user just always got the manual-command notice. Replaced withTerminalToolWindowManager.createNewSession(workingDirectory, tabName, shellCommand, requestFocus, deferSessionStartUntilUiShown), verified by hand to exist on 251, 252 and 262 alike. The login is now passed as an argv list rather than a shell string, which also removes the shell-quoting hazard (the Windows PowerShell&prefix, paths with spaces) and the send-text-into-a-shell startup race that could swallow the command. -
The native PTY sign-in was unreachable code.
startLogin()called the terminal path unconditionally, soClaudeLoginFlow— the pty4j-based flow the KDoc and docs described as the primary path — was never invoked, and there was no fallback when the terminal failed./loginnow tries the IDE terminal, then the native PTY flow (which needs no Terminal plugin at all), and only then the manual notice; each step is a real fallback rather than a dead end. Fixed a latent bug in that path while wiring it up: pty4j replaces the child environment wholesale (unlikeClaudeProcess, which inherits the parent's), so the base environment is now merged in — without it the spawned binary would have lostPATH/HOMEentirely.
- Added
TerminalApiContractTest, which pins thecreateNewSessionoverload against the real platform on the build classpath so a future rename fails at build time instead of silently degrading. Documents why CI missed the original break: the plugin compiles/tests against IC-2025.2 (252), where the removed factories still exist, while the regression only manifests on 262+ — an asymmetryverifyPlugin's range run is the complementary guard for.
- Per-rule security toggles (Settings ▸ Claude Code ▸ Security).
SensitiveGuard's three categories — Credential/key material, Dangerous commands, and Foreign territory (now split into its three sub-rules: another user's home, network/UNC mounts, and foreign WSL drives) — are each independently switchable, all ON by default so a fresh install reproduces the original behaviour exactly. Turning a rule off is never a silent allow:classify()still runs unconditionally, and a hit is only downgraded from an automaticDENYto a permission card (ASK) — shown every time, to every caller, MCP servers and Skills included. A trusted agent tool that trips Credential/Dangerous-command already got a card either way; the toggle only ever changes what an untrusted caller gets.SensitiveGuard.reason()now always names where to change the rule ("… — disable this in Settings ▸ Claude Code ▸ Security"), whether the rule is currently enforced or already downgraded, so the lever is discoverable from the block/prompt itself.Policygained fiveenforce*fields (all defaultingtrue);ClaudeSettingspersists the five toggles and wires them throughsensitivePolicy().
- Native CLI tools the plugin didn't know about were hard-denied like a blocked MCP server.
SensitiveGuard.AGENT_TOOLS— the allowlist of trusted, first-party callers — had gone stale as the CLI grew its own orchestration surface: the background-task family (TaskCreate/TaskGet/TaskUpdate/TaskList/TaskOutput/TaskStop), cron (CronCreate/CronDelete/CronList/ScheduleWakeup), worktrees (EnterWorktree/ExitWorktree),EnterPlanMode,Agent(a newer alias forTask),SendMessage, the MCP-resource-browsing tools (ListMcpResources/ReadMcpResourceDir/ReadMcpResource/RefreshMcpTools), and several more were all missing from the list — none of them are third-party (they're modeled in the vendored@anthropic-ai/claude-agent-sdkreference alongsideBash/Read/Edit, not user/community-authored like a Skill or an MCP server) — so tripping the Credential or Dangerous-command rule denied them outright instead of asking, indistinguishable from a genuinely blocked MCP call.AGENT_TOOLSnow includes the full confirmed set;Skilland anymcp__*-prefixed name remain deliberately excluded (that content is third-party by design, unaffected by this fix). Regression test covers all 31 newly-trusted tool names.
- The model picker is now fully driven by the binary's own catalog, with the version on every entry. The list was already autodetected from the
initializehandshake, but it labelled each model with the binary'sdisplayName— which omits the version ("Opus (1M context)", "Sonnet") — so you couldn't tell Opus 4.8 from Opus 5 at a glance. Each entry now shows the versioned label the binary carries in itsdescription("Opus 5 with 1M context", "Sonnet 5", "Haiku 4.5"). The same label logic backs both the composer pill/menu and the Settings combo, so they never disagree. - The floating "default" alias is no longer offered as a selectable model, and the default is pinned to the concrete Opus tier. The binary exposes both a
defaultalias and the concreteopus[1m]value that it currently resolves to — the same model listed twice, the alias with no version. The alias is now filtered out of both selectors, and a fresh install defaults to the concrete Opus (ClaudeSession.DEFAULT_MODEL), so the choice stays on Opus even if the binary later re-points its recommendation.preferredDefaultfalls back to the binary's own recommended alias (then to the first listed model) if a binary ever ships without the pinned value, so the plugin never selects a model the binary doesn't offer. A legacy install withdefaultpersisted is migrated to the concrete tier on display/save.
- Removed a hardcoded
"Default · Opus 4.8"model label. The composer pill fell back to that literal string whenever the selected model was unset or thedefaultalias — which went stale the moment the recommended tier moved to Opus 5, showing "Opus 4.8" for what was actually Opus 5. The label is now always derived from the live catalog (or from the model id as a last resort), never a baked-in version.
- Protocol drift baseline advanced to SDK
0.3.220/claude2.1.220(./gradlew checkDriftgreen; protocol surface unchanged).
- The command a
Bash/PowerShell/MCP-exec call runs is now its own copyable code block, shown right under the tool card's header — visible without expanding the card, with a Copy button (SensitiveGuard.commandTextdetects it by input shape, not tool name, so any command-executing tool is covered). The header no longer crams the raw command text into the title; it just names the tool, and the card gets a distinct left-accent look (cmd-tool) so a command call reads as one at a glance. - Syntax highlighting for diffs and file output. A
Read/Write/Edit/MultiEditcard's plain output, and the coloured unified diff on a completed edit, are now syntax-highlighted from the file's extension (CC.languageForPath, ~35 languages from the vendored highlight.js bundle), layered under the existing added/removed line colouring for diffs. Falls back to highlight.js's own autodetection for an unrecognised extension.
- Fixed a false positive that hard-denied ordinary
Edit/Writecalls.SensitiveGuard.isUncclassified any string starting with//as a UNC network path — including an everyday// some commentline inside anEdit'sold_string/new_string(pathCandidateswalks every string leaf of the input, not just recognised path keys). That misclassified the call as foreign territory, which denies outright regardless of caller trust — so editing a file with a//-style comment on the touched line could get silently refused, with no setting to override it. Fixed:isUncnow requires the segment right after the leading//to be a whitespace-free, non-blank host name, which a real UNC path always has and a comment line never does. Regression tests added. - Fixed a crash in the sensitive-command classifier.
SensitiveGuard.substituteAssignmentspassed a shell-assigned value straight toString.replace(Regex, String), which treats its second argument as a replacement template ($1/${name}are group references, not literal text) — a value containing$/${...}(e.g.k=${OTHER}/x) threw an uncaughtIllegalArgumentException: Illegal group referencefrom deep insidejava.util.regex.Matcher, confirmed live via a stack trace, crashingverdict()for thatBashcall with no response ever sent back to the binary. Fixed withMatcher.quoteReplacement, so the value is always substituted literally. Regression test added. - WSL: the security layer no longer refuses to start on a
/mnt/cproject. WSL2 mounts the WindowsC:drive over 9p, which is inRemoteMounts.REMOTE_FS_TYPES, sodetect()put/mnt/cintoremoteRootsand the startup gate (RemoteMounts.isRemote) treated a normalC:\project as a network share and aborted the launch; the sameremoteRootsalso fedSensitiveGuard's foreign-territory rule. Fixed in two layers:detect()no longer treats any/mnt/*mount as a generic remote root under WSL (those are governed by the dedicated/mnt/crule), andisRemoteexempts/mnt/c(and its subtree) before the fstype checks as defense in depth. Every other/mnt/*drive stays foreign. Regression tests added (RemoteMountsTest).
Jump to code from the conversation, a chat tab that actually takes the keyboard focus, and an IDE that sees Claude's writes as they happen.
- Jump-to-code links in the transcript. A file tool's card names its file relative to the project (
Read(src/app/session.py), not a bare file name) and the path is clickable: it opens in the editor at the right line and is selected in the Project view. In model text, paths (src/Foo.kt,a/b.py:42,~/.claude), directories (revealed and expanded in the Project view — or opened in the OS file manager when they live outside the project) and symbols (PermissionBroker, resolved through Go to Symbol, so it works in every JetBrains IDE, not just the Java/Kotlin ones) become links as well. A bare file name resolves too (app.css:190— via the IDE's file index, plus a bounded on-disk scan for excluded folders likebuild/, which no index knows about), and archives reveal in the tree instead of opening a useless binary buffer. - Nothing is linked on a guess: the IDE confirms every candidate first, and only an unambiguous match links — two
app.cssin the tree means no link at all, rather than a jump to an arbitrary one. Anything unresolvable stays plain text, so a link is never dead.
- Compatibility floor lowered to build 251 (2025.1), from 252. That is as far back as the plugin reaches while shipping zero deprecated API:
FileChooserDescriptorFactory.multiFiles()/singleDir()(the Attach file picker) simply does not exist on 2024.2/2024.3 — verified,NoSuchMethodError— and its pre-251 equivalent is deprecated on current IDEs. Reaching 2024.x would need a separately targeted build, which is JetBrains' documented approach for a range where the API actually changed. Verified Compatible on IC-251, IC-252, IU-253, IU-261 and IU-262, with no internal-API and no deprecated-API usage. verifyPlugincan now run fully offline:-PlocalIdePath(andLOCAL_IDE_PATH) accept a comma-separated list of extracted IDEs, so the whole declared range can be verified without reachingdownload.jetbrains.com— which matters, because the verifier is the only thing standing between a clean compile and aNoSuchMethodErrorin a user's IDE.
- Added
permission/SensitiveGuard, a deterministic pre-authorization gate forcan_use_tool. It is evaluated inPermissionBroker.handlebefore any auto-approval branch, so it applies regardless of permission mode (default/acceptEdits/bypassPermissions) and regardless of "Always allow" — the binary is always launched indefaultmode (SessionLauncher.binaryPermissionMode), so every call is delivered as a control request and the verdict is the plugin's to make. Not a model-side guardrail: the classification is out-of-band Kotlin with no model input.- Classification (three categories). Credential/key material — SSH/GPG/PKI, cloud/cluster/container credentials, DB and shell-history secrets, browser and password-manager stores, crypto wallets, and AI-agent/code-host access tokens — matched by structural globs (
**/…) rather than$HOME-anchored, so native, macOS, Windows (C:\Users\*) and WSL (/mnt/c/Users/*) paths resolve to one rule. Dangerous commands — credential dumps, file exfiltration, reverse shells, LOLBINs, recognised offensive tooling — matched against a curated regex set. Foreign territory — another user's home (/home/*,/Users/*,/root), UNC (\\host\share), a network mount (fstype ∈ {nfs, cifs, sshfs, …} viaRemoteMounts), or under WSL any/mnt/*≠/mnt/c. - Input coverage. The full input object is walked for path-like string leaves (not a fixed key list), so an MCP tool naming its argument
path/target/destination/… is covered. Command strings are extracted from command-shaped keys andargvarrays. - Evasion resistance. Path candidates are canonicalized on disk via an injected resolver (symlink and
..targets), and command strings pass a de-obfuscation stage (quote-splitting,$IFS, single-token variable substitution,base64-payload decode) before matching. Both raw and normalized forms are evaluated. Detection of paths inside arbitrary shell strings is best-effort by design; enforcement of a match is not. - Verdict matrix (allowlist by caller). Trusted caller = the agent's built-in tools only. Credential/command hit → trusted caller ASK (card shown in every mode); untrusted caller (MCP/Skills/unknown) DENY. Foreign-territory hit → DENY for all callers. No setting relaxes these; the only user knob is
sensitiveExtraGlobs, which is additive to the built-in blacklist. - Scope exemption. Paths under the project root are exempt from the credential and foreign-territory rules (the sanctioned working zone); dangerous-command classification is location-independent.
- Startup gate.
ClaudeSession.startrefuses to launch whenproject.basePathresolves to a remote/network/foreign mount (RemoteMounts.isRemote), surfacing an error notification instead of spawning the process. - Covered by 35 unit tests (
SensitiveGuardTest,SensitiveGuardEvasionTest,RemoteMountsTest), including negative cases for ordinary development. SeeSECURITY.md.
- Classification (three categories). Credential/key material — SSH/GPG/PKI, cloud/cluster/container credentials, DB and shell-history secrets, browser and password-manager stores, crypto wallets, and AI-agent/code-host access tokens — matched by structural globs (
- Jump-to-code links are gated by
LinkResolver.isOpenable: a link can only ever point inside the project or inside the user's own home — never at/etc/passwd, never at another user's files, not even through a symlink (the check compares canonical paths). The write gate is untouched: what the binary may write stays confined to the project root.
- A chat tab could come up unusable — the composer refused to take the keyboard focus (a newly opened tab, and sometimes the tabs restored at IDE start); the only cure was closing and reopening the tool window. Two independent causes: the tab never declared where its keyboard focus lives (
Content.preferredFocusedComponent, which must point at CEF's real input component —JBCefBrowser.getComponent()is a wrapper panel and is not focusable), and a raw AWTrequestFocusInWindow()is refused outright while the IDE's ownIdeFocusManageris settling focus (measured: denied 34 times in a row on a fresh tab). The focus is now transferred by theContentManageras part of selecting the tab (setSelectedContent(content, requestFocus = true)) — the same path a manual tab switch takes. - No caret in a new chat tab, even though the keystrokes were arriving. CEF keeps its own focus flag, and a freshly loaded page starts with it cleared — while the browser takes the focus ~500 ms before its page exists. It is now told it has the focus once the chat has actually announced itself (
JcefHost.markWebReady), which is when there is a caret to paint. - The IDE only saw Claude's writes at the end of a turn. Until then the editor showed stale contents and a jump-to-code link on a freshly written file opened nothing at all — the file did not exist for the IDE yet. Every successful write now refreshes the VFS immediately: by exact path for
Edit/Write, and by re-scanning the project tree after aBashcommand or a file-mutating MCP tool, which can change anything. Newly created files are picked up too — refreshing a file the VFS has never heard of is a no-op, so its parent directory is re-scanned as well. - Restored sessions showed absolute paths on their tool cards, with no links.
SessionTranscriptReaderrebuilt the transcript without the project root, so reopening the IDE turned every card into a bare absolute path. A separate code path from a live session — and it had been missed.
PluginId.getId(…)is gone.PluginIdbecame a Kotlin class in 2025.2, so compiled against that SDK the call binds toPluginId.Companion— a symbol that does not exist in older IDEs, i.e. aNoSuchFieldErrorwaiting to happen anywhere below 252. The id now comes from the plugin descriptor (InstalledPlugins), which reads the same on every IDE. Caught byverifyPluginagainst IC-251, not by the compiler: it is a binary incompatibility, not a source one.LinkResolver.resolveSymbolsmoved off the deprecatedReadAction.computetoReadAction.nonBlocking(…).inSmartMode(project), which also fixes a real bug: the Go to Symbol index does not exist while the IDE is indexing, so symbols would have silently resolved to nothing.- Tests:
+37(Kotlin 588, frontend 32, 0 failures). The newLinkGateTestcovers the security boundary as a boundary — project file, home file,/etc/passwd,/usr, another user's home, a../../..traversal and a symlink pointing at/etc— and the frontend suite now guards the CSS-specificity trap that painted jump-to-code links in the accent colour inside model text while the identical ones on tool cards came out blue.
Protocol upgrade to claude 2.1.204 / SDK 0.3.204 — ./gradlew checkDrift flagged five new protocol kinds; reconciled and re-verified green at the new baseline.
system/background_tasks_changedis now modeled and surfaced as a "Background tasks" card in the session dashboard (with Stop). It's a level signal — the binary re-sends the full live set on every membership change — so unlike the edge-derived Subagents list it can never wedge a stale "running" indicator on a missed start/stop bookend. Kept deliberately separate from the subagent stream (the SDK leaves their relative ordering unspecified and forbids correlating them); reset to empty whenever the CLI process restarts.system/control_request_progressis now modeled: progress for a host-originated control request (currentlyside_question, i.e./btw). Anapi_retrystatus carries the same retry counters assystem/api_retryand is surfaced the same way instead of being dropped;startedis logged.
- Empty "Thought process" fold on Opus 4.8. Newer models emit redacted thinking: the block streams only a
signature_deltaand everythinking_deltacarries an empty string (verified on the wire: 4/4 deltas empty, finalized blocklen=0).str()returns""— which is notnull— so the parser's unguarded?.letemitted a delta and opened a "Thought process" fold with nothing in it, which never filled. Empty thinking deltas no longer produce an event, a blank delta never opens a fold, and a blank finalized block never blanks out reasoning that did stream. There is simply no fold when there is no reasoning text to show. - MCP servers card layout was broken.
.mcp-actionshad no CSS rule at all, so the Reconnect button and the enable/disable switch wrapped onto separate lines and overlapped; and the switch (a 32×18 pill whose knob is an absolutely-positioned::after) was given a text label, so the knob painted on top of it (Dis●ble). The actions row is now a proper flex row, the switch is a switch (state viarole="switch"/aria-checked, name viatitle/aria-label), and the server name getsmin-width: 0so it ellipsizes instead of shoving the buttons out of the row at narrow widths.
- Triaged three thin-client host→binary control requests the plugin knowingly does not send —
list_models(models come from theinitializereply),get_plan,get_workspace_diff— intoProtocolSurface.KNOWN_SUBTYPES. - Baseline bumped to
sdk=0.3.204/binary=2.1.204;checkDriftgreen.
- Editable diff review for edits. When Claude asks to Edit/Write/MultiEdit a file, the plugin now auto-opens an editable diff in the IDE editor (Current | Proposed) — not just in acceptEdits/bypass mode. The proposed side is created with
DiffContentFactory.createEditable, so you can tweak the proposed content right in the editor before approving; Accept writes your edited version (the tool input is re-encoded viaHunkSelection.encodeInputso the binary writes exactly what you left), and the diff closes automatically on accept/reject. The captured snapshot is repointed at the effective input, so the transcript's inline diff and "View diff" reflect what was actually written (your edit), not Claude's original proposal. Fail-safe: if you change nothing — or the platform renders the proposed side read-only — Accept falls back to writing Claude's original proposed content, never a wrong write. Review diffs are also closed on stop/interrupt/dispose.
- Permission cards for edits now show a read-only diff instead of per-line checkboxes. The previous hunk-by-hunk partial-acceptance UI (a checkbox per changed region) rendered as a confusing checklist and, worse, let you apply an incoherent subset of an edit — a reliable way to produce broken code. Edits are now atomic: the card shows a proper colour-coded unified diff (red removed / green added) and you accept or reject the whole change. The full diff is still available via View diff and the IDE's auto-opened diff tab. The partial-accept plumbing (
hunkCache, per-hunk encode/reconstruct in the card path) was removed.
A broad bug-fix + UX pass (the claude binary auto-updated to 2.1.193 in the meantime; protocol re-baselined).
- Interrupt never actually stopped. Esc / the Stop button sent the
interruptcontrol request fire-and-forget with no response handler, so the binary's ack was discarded,turnActivenever cleared, and the "Interrupting…" line — added as a permanent transcript row — re-rendered on every state push (the looping "Interrupting" the turn never escaped). Interrupt now goes through the correlated control client (clears the turn on ack/timeout), shows a transient Interrupting… state on the Stop button (no transcript spam), flushes the queued prompts so it can't immediately re-pump a new turn, and clears pending permission cards. - Chat dead on first open (had to close & reopen the tab). The JS
readyhandshake could fire before the host injectedwindow.__ccSend, dropping the message; and a process-global scheme race could serve a blank page. The handshake is now self-healing: the web app retriesreadyuntil the bridge exists, and the host reloads vialoadHTMLif the page doesn't come alive shortly after load. - User prompts were rendered as Markdown. A prompt containing
*,#, backticks or indentation got mangled. User messages now render verbatim (plain text, whitespace preserved); Markdown rendering is reserved for model output. - Dead "Copy" button on model code blocks — the per-block click listener was lost when the decorated fragment was serialized to HTML. Copy is now a delegated handler (click + keyboard).
- "Thought process" duplicated / out of order. A finalized thinking block was appended as a second, post-answer entry instead of replacing the streamed one. Reconciliation now tracks the message's thinking entry and replaces in place.
- Menu flicker + de-selection while streaming. The composer rebuilt pills and closed/reopened the open menu on every (frequent) state push. It now updates incrementally and only rebuilds an open menu when its selection actually changed. The transcript no longer re-serializes the whole conversation on every appended row (was O(N²)).
- Two checkmarks on the selected item in prompt menus (CSS
::after+ a JS span) — now one. - Esc closing the find bar also interrupted the turn — the find-bar Escape now stops propagation.
- "Always allow" could approve the wrong pending card (it matched by tool name); it now resolves the exact card. Accepting zero hunks is treated as a deny (it used to send a no-op edit the model saw as an error).
- Permission re-push wiped in-progress card state (typed elicitation fields, question selections, unticked hunks) — cards are now reconciled by id.
- Session dashboard layout was broken (missing
.dash-innergrid wrapper; the overlay covered the composer) and now lays out correctly without hiding the composer. - Clipboard paste froze the IDE on a slow/hung clipboard owner (the Wayland
wl-paste/xclipread ran on the EDT with an unbounded read). Reads now run off-EDT with a deadline. - Find bar now scrolls to the active match and supports Enter / Shift+Enter navigation with an
i / ncounter.
- Adaptive thinking is ON by default for new installs.
- Vibe Mode rainbow is ~3× faster (and coherent between the JCEF and Swing sides).
- Responsive UI: the composer control bar wraps instead of clipping pills, the find bar and chips are fluid, and chat tab titles are truncated (full title in the tooltip) so many open chats don't push the tab strip off-screen.
- Latent concurrency/lifecycle fixes: a
startingguard + generation re-checks prevent a doubleclaudespawn and an orphaned process when a tab is closed mid-launch;dispose()now bumps the generation (no spurious "exited unexpectedly"); a malformedcan_use_toolcan no longer throw and hang the turn (it replies with an error); theToolWindowFactoryno longer caches a per-project window in shared state. - Protocol re-baselined to
claude2.1.193 / SDK 0.3.193 — models the newsystem/informational,model_refusal_no_fallbackandworker_shutting_downsubtypes;./gradlew checkDriftgreen.
- Composer clipboard paste on native-Wayland IDEs (the real fix). 4.0.2 added a host-side
wl-pasteread fallback, butCtrl+Vstill did nothing — because the bug is the trigger, not the read. Under the native Wayland toolkit (sun.awt.wl.WLToolkit) the embedded CEF browser's web clipboard is isolated from the system clipboard, so the composer'spasteevent only ever saw content copied inside the web view, and never reached the host.Ctrl+Vnow routes through the host whenever the Wayland toolkit is active (ahostClipboardflag in the meta payload): the paste handler ignores CEF's isolatedclipboardDataand the host reads the real clipboard viawl-paste/xclip— the same path the Attach → Image button already used successfully. Text and image paste from external apps now work, as does pasting back what a Copy button placed on the system clipboard.- Diagnosis (confirmed live): even the IDE's own editors can't read the external clipboard under this JBR (AWT/
CopyPasteManagerreads are broken on native Wayland — a focus-gated protocol limitation), sowl-paste(thedata-controlprotocol) is the only mechanism that reaches the Wayland clipboard for reads. Clipboard writes (the Copy buttons →CopyPasteManager.setContents) already worked.
- Diagnosis (confirmed live): even the IDE's own editors can't read the external clipboard under this JBR (AWT/
- Text paste broken on native-Wayland IDEs. On IntelliJ 2026.1+ running the native Wayland toolkit (
sun.awt.wl.WLToolkit), AWT's clipboard is empty/unreliable, soCtrl+Vof plain text into the composer did nothing (image paste already worked — it had awl-paste/xclipfallback; text didn't). Text paste now falls back to the same host-side CLIs, reading a realtext/*target. The selection is guarded (preferredTextType) so an image-only clipboard — e.g. a KDE screenshot, where a blindwl-paste -nemits raw PNG bytes — is never mis-read as text, andtext/uri-list(a copied file) andtext/html(markup) are excluded from the plain paste. X11/XWayland and Windows/macOS are unaffected (AWT works there, so the fallback never triggers).
Protocol upgrade to claude 2.1.170 / SDK 0.3.170 — ./gradlew checkDrift flagged four new protocol kinds; reconciled and re-verified green at the new baseline.
system/model_refusal_fallbackhandling. When the primary model ends a turn with stop_reasonrefusal, the binary now retries once on a fallback model and emits this system message. The plugin models it (ModelRefusalFallbackInfo) and surfaces a transcript notice ("The model declined to respond (<category>) → retried on <fallback-model>.") instead of silently dropping the frame. Previously the parser left it asOther, so a refusal-and-retry was invisible.
- Drift baseline →
claude2.1.170 / SDK 0.3.170. Triaged the three new host→binary control requestsget_usage,register_repo_root, andreload_skillsinto the known protocol surface (ProtocolSurface.KNOWN_SUBTYPES) — the plugin doesn't send them yet, but they're no longer reported as drift.checkDriftis green at the new baseline.
Chat UI rebuilt on JCEF (embedded Chromium), then hardened and extended — all frontend; the Kotlin backend was untouched. See RELEASE_NOTES.md for the full story.
- Embedded-web chat (JCEF): streaming transcript, web composer, native permission/question/elicitation cards, session dashboard, strict hash-pinned CSP.
- Hunk-by-hunk partial diff acceptance — checkbox per changed region on reviewable Edit/Write/MultiEdit cards.
jb://jump-to-code links —@filementions open the file at the line, gated to the project root.- Rich attach menu — search + Files/Directory/Image + current selection/file + filterable Recent files.
- Syntax highlighting in the IDE's colours (highlight.js classes mapped to the editor scheme).
- Native rewind as the default rollback (
rewind_filesby turn) with a confirmed IDE-side per-file fallback. - Clipboard paste on Wayland (text via AWT, image via
wl-paste/xclip), tool-card colour states, colourised inline diffs, Ctrl+O reasoning toggle, auto-follow, 🌈 Vibe Mode, inline images, responsive layout.
- The old Swing chat UI (
ChatPanel/TranscriptView/MarkdownRenderer+ tray/strip panels) and its tests were removed. - ⚙ menu reuses the formatted JCEF dashboard instead of plain-text dialogs.
- Migrated the rewind-fallback confirmation off the deprecated
Messages.showYesNoDialog(…DoNotAskOption)overload toMessageDialogBuilder.yesNo(keeps the zero-deprecation build clean).
- Startup crash / "all sessions disappeared" regression (introduced by the
hunkCacheleak fix below, before release). The new unconditionalhunkCache.keys.retainAll(…)prune inpushPermissions()dereferencedhunkCache, but that field was declared after theinit {}block that callspushPermissions()— and Kotlin initializes properties in declaration order, so the field was stillnullduring construction →NullPointerExceptioninJcefChatPanel.<init>. Every chat tab (including each restored session on startup) failed to construct, leaving the tool window empty (sessions on disk were untouched — the binary's JSONL files are the source of truth). The field declaration moved aboveinit {}. The earlier code only touchedhunkCacheinside thecomputeHunksloop, which is empty at startup, so the null-deref stayed latent until the unconditional prune was added. - Hunk-by-hunk partial accept no longer writes from a stale snapshot. On accept the file is re-read from disk; if it diverged since the card was shown, the plugin falls back to a normal full accept instead of reconstructing from the cached line snapshot (which could silently no-op or clobber an external change).
hunkCachecan no longer leak. Cached hunk contexts are pruned to the still-pending permissions on every push and cleared on panel dispose, so permissions cleared on stop/interrupt (without an explicit resolve) don't accumulate.- Large files skip the EDT-side hunk read/diff (>1 MB) — hunk-by-hunk review is meaningless there and the synchronous read would freeze the UI; full accept still works.
- Restored the
sms:URI scheme in the DOMPurify allowlist (it was dropped when the explicitALLOWED_URI_REGEXPreplaced DOMPurify's default;data:image/inline images and the internaljb:scheme remain allowed,data:text/htmlstays blocked).
- Mermaid / KaTeX — avoided as external bloat that would force relaxing the strict CSP. The plugin stays lean (~1.6 MB).
Completes the binary→host protocol surface. Every message and control request the claude binary sends the host is now both parsed and used: the two control requests that were previously rejected with an error are answered correctly, and every event that was parsed-but-only-logged is now surfaced in the GUI. After this release nothing the binary emits to the host is silently dropped or wrongly errored — it is acted on (when it is a request) or shown (when it carries information). A new on-demand drift detector keeps these native models in lock-step as the binary and its SDK evolve, with KNOWN_SUBTYPES now tracking the full triaged 0.3.162 subtype surface (receive + send + knowingly-triaged).
- MCP elicitation — native input cards. When an MCP server asks the user for input (
elicitation), the plugin surfaces it as an inline non-modal card in the permission tray (never a blocking dialog), reusing the samePendingPermissionpipeline as the question/plan cards. URL mode (e.g. an OAuth flow) shows an Open link button + Accept/Cancel; form mode renders a labeled input per primitive field of the server'srequested_schema(string/number/integer/boolean — extracted by the pureparseElicitationFields) and returns the collectedcontenton Accept, else Decline; a non-renderable schema degrades to a plain Accept/Decline. Replies with anElicitResult({action, content?}). Previously rejected with an error. Tearing down a session with a card pending default-cancels it (while the process is still alive) so the binary is never left waiting. (protocol/Protocol.kt,protocol/ClaudeEvent.kt,protocol/ControlProtocol.kt,permission/PermissionBroker.kt,ui/PermissionTrayPanel.kt,session/ClaudeSession.kt.) request_user_dialoganswered correctly. A tool-driven blocking dialog of an open-union kind the host doesn't render is now answered{behavior:"cancelled"}(the CLI then applies the dialog's own default) with a brief transparency note, instead of an error reply. The pureDialogResponderowns the reply + note so it stays unit-testable. (protocol/DialogResponder.kt,protocol/ClaudeEvent.kt,protocol/ControlProtocol.kt,session/ClaudeSession.kt.)
Added — surfacing previously parsed-but-hidden events in the UI
- Predicted next-prompt chip. The binary's
prompt_suggestionnow appears as a dismissible💡chip above the composer; clicking it fills the input (you review/edit — never auto-sent), and it clears on send / dismiss / new turn. (ui/SuggestionStripPanel.kt,ui/ChatPanel.kt,session/ClaudeSession.kt.) - Live reasoning-token estimate. The composer status line shows the running
thinking_tokensestimate mid-turn (e.g. "Pondering… · ~1.2k reasoning tokens"), bucketed so it doesn't flicker, and reset at each message boundary and on teardown. (session/StatusLineFormatter.kt,ui/ChatPanel.kt,session/ClaudeSession.kt.) - Native hook execution rows. The binary's hook telemetry (
hook_started→hook_progress→hook_response) is narrated as one evolving transcript row per hook (running, with the latest output line → ✓/✗ on completion), keyed by hook id so a chatty hook can't flood the transcript. Distinct fromHookBroker, which answers thehook_callbackcontrol request. (session/HookActivityNarrator.kt,session/ClaudeSession.kt.) - Memory-recall row.
memory_recallsurfaces as a collapsible "Recalled N memories" row listing each recalled memory (scope · path + snippet), so it's visible what context influenced the turn. (session/MemoryRecallFormatter.kt,ui/ChatMessageViews.kt,session/TranscriptModel.kt,session/ClaudeSession.kt.) - Tool-use summary + file-upload notices.
tool_use_summaryrenders as a quiet dim note;files_persistednow also confirms successful uploads (not only failures). (session/ClaudeSession.kt.)
./gradlew checkDrift— an on-demand Kotlin task that updates the vendored SDK + theclaudebinary to latest first (npm update+claude --update), then diffs the live protocol surface (subtype literals fromsdk.d.ts+ a one-turn binary probe) against the plugin's triagedKNOWN_EVENT_TYPES/KNOWN_SUBTYPES, printing an agent-consumable markdown report and failing on actionable drift (a bare version bump with a covered surface passes). Pure extraction/diff is offline unit-tested; the live half is taggeddriftLiveand excluded from the normaltesttask. Runbook indocs/DRIFT_DETECTION.md.
- New single-responsibility collaborators, keeping
ClaudeSessiona thin delegating orchestrator (no god-object regrowth):HookActivityNarrator(hook-row state machine), and the pureMemoryRecallFormatter/StatusLineFormatter/protocol/DialogResponder. TheonEventdispatch now routesMemoryRecall,PromptSuggestion,ThinkingTokens,HookStarted/Progress/Response,ToolUseSummary,FilesPersisted,UserDialogRequestandElicitationto these instead oflog.debug. (session/.) - New composer sub-panel
SuggestionStripPanel(autonomous, likeQueueStripPanel); new transcript kindSpeaker.MEMORY+ a collapsibleMemoryRow(its own toggle, not driven by Ctrl+O);PermissionTrayPanelgains an elicitation-card branch andPendingPermissioncarries an optionalElicitationCard. (ui/,permission/PermissionBroker.kt,session/TranscriptModel.kt.) - Protocol baseline → SDK
0.3.162/claude2.1.162, andKNOWN_SUBTYPESexpanded to the full triaged surface (every subtype the plugin parses, answers, sends, or knowingly leaves asOther/UnsupportedControlRequest).
- MCP elicitation URLs are scheme-restricted. An MCP server is untrusted, so a
url-mode elicitation link is opened only when it ishttp/https—file:/jar:/javascript:/UNC and other schemes are never handed to the browser launcher (gated both in the tray, which won't even offer the button, and at theBrowserUtil.browsecall site, mirroring the link-scheme allow-list used elsewhere in the UI). Form-input values are built as a plaincontentobject of the user's typed values; the replyactionis constrained to accept/decline/cancel. (ui/PermissionTrayPanel.kt,ui/ChatPanel.kt.)
- New: control-protocol builders (
userDialogCancelled/Completed,elicitationResult), control-request parsing (request_user_dialog+elicitation, malformed→fallback),parseElicitationFields(primitives / nested→empty / null→empty),DialogResponder,StatusLineFormatter,MemoryRecallFormatter, theHookActivityNarratorstate machine, and a headlessClaudeSessionevent-surfacing suite via thehandleEventForTestseam. Full non-UI pyramid green;verifyPluginCompatible across IC-251/252 and IU-253/261/262-RC.
- API provider selector (Anthropic / DeepSeek) — a new
Provider:setting (Settings ▸ Claude Code) and a composer chip pick the endpoint theclaudebinary talks to. Anthropic uses the binary's own native login (subscription/OAuth). DeepSeek routes to its Anthropic-compatible endpoint (https://api.deepseek.com/anthropic) and requires its own issued key. Each provider keeps an isolated API key in the IDE password safe (not inclaude-code.xml), shown with its brand logo on the chip and in the menu. (settings/Provider.kt,settings/ClaudeSettings.kt,ui/ClaudeSettingsConfigurable.kt,ui/OptionMenus.kt,ui/ChatPanel.kt,ui/ChatTheme.kt,resources/icons/provider-*.svg.)
- Reasoning toggle now persists across turns — new "Thought process" blocks correctly inherit the Ctrl+O toggle state instead of always appearing expanded. Previously toggling reasoning off hid existing blocks but every new turn's reasoning popped open again. (
ui/TranscriptView.kt)
- Credentials are pinned to their provider — no Anthropic credential ever leaks to a third party. Switching provider sets
ANTHROPIC_BASE_URLandANTHROPIC_API_KEYas an atomic pair, and ONLY when a key is present (never a lone base URL, which would make the SDK ship your Anthropic OAuth bearer to the other endpoint). BecauseANTHROPIC_API_KEYis set, the binary's SDK does not even load the stored OAuthcredentials.json, so the subscription can't be sent elsewhere. We never emitANTHROPIC_AUTH_TOKEN. The settings form rejects an Anthropic-shaped key (sk-ant-…) for a third-party provider; selecting a third-party provider with no stored key does not switch or restart — it prompts to configure the key first./loginis restricted to the Anthropic provider (a third-party auth failure is a wrong key, not a missing OAuth login). The pureProvider.launchEnvrules are unit-tested. (settings/Provider.kt,session/ClaudeSession.kt.)
- Two-row, adaptive options bar — the composer controls are split into row 1 (model · mode · effort · thinking pills, centred) and row 2 (the toggles + attach, centred, with the Play/Stop button right-aligned). Each pill is a flat capsule with its own category glyph and a coral hover glow; the value label is now just the live value (full name in the tooltip). The Send control is a thin neon outline glyph (triangle/stop-square) stroked in the accent. (
ui/ChatPanel.kt,ui/ChatTheme.kt,resources/icons/chip-*.svg.) - Coral focus ring + editor-font prompt — the composer card lights its border coral (with a soft halo) while the prompt is focused, and the prompt now uses the IDE editor font (typically a mono) at the UI-scaled size, for a code-native feel. (
ui/ChatPanel.kt,ui/ChatTheme.kt.) - Output follow toggle — a follow button (coral while active, on by default) force-follows the streaming bottom even if you scroll up; off, the transcript still follows naturally while you're parked at the bottom so you can read history mid-stream. (
ui/ChatPanel.kt,ui/TranscriptView.kt.) - 🌈 Vibe Coder Mode — an opt-in gag toggle that animates the coral accent through the rainbow: the send glyph, option pills, every bordered box, the tool/chip/attach icons (retinted live), the prompt's vibe ring, and the avatar (a Nyan Cat, with the tool-window stripe icon swapped to match). Off by default; one timer, stopped on dispose. (
ui/ChatPanel.kt,ui/ChatTheme.kt,resources/icons/chip-follow*.svg,resources/icons/claude-vibe.svg.) - Composer paste fixed (Ctrl/Cmd+V) — pasting an image into the composer now works, including the Linux case (Wayland over XWayland / X11) where a clipboard image arrives as a raw
image/…flavor (InputStream/byte[]) rather thanDataFlavor.imageFlavor. The keyboard paste is bound through the IDE action system (aDumbAwareActionon the platform$Pasteshortcut viaregisterCustomShortcutSet, so it honours the user's keymap and is correct on every OS), and clipboard access goes through the cross-platformCopyPasteManager. A latent bug where the drag&dropTransferHandlercaptured a null delegate (breaking text paste/drop) is also fixed. New pure, unit-testedcontext/ClipboardImageReadercentralizes raw-image extraction; the rendered-java.awt.Imagefallback stays in the composer. (ui/ChatPanel.kt,context/ClipboardImageReader.kt.) - Richer attachment menu — the attach menu now offers, besides current file / selection / clipboard image: Add files… and Add directory… (native
FileChooser), plus Add open files… and Add recent files… submenus (fromFileEditorManager/EditorHistoryManager). File chips use a root-relative label so same-named files in different folders no longer collide on dedupe. Newcontext/FilePickerHelper(the puredisplayNameis unit-tested). (ui/ChatPanel.kt,context/FilePickerHelper.kt.) - Native visual identity — a custom 16×16 SVG icon set replaces the borrowed
AllIconson tool-call rows (bash/read/edit/search/web/task/generic), the attach button, and attachment chips; file attachments show their real file-type icon. Attachment chips gain hover highlight, hand cursor, a custom ✕, and click-to-open in the editor (project-confined). (resources/icons/*.svg,ui/ChatTheme.kt,ui/ChatMessageViews.kt,ui/AttachmentStripPanel.kt,ui/ChatPanel.kt.) - Diff History tab + rollback — a toolbar action opens a Diff History tab listing every reviewable Edit/Write/MultiEdit in the session (root-relative path, tool, native
+a/-bsummary) with View diff and per-edit Revert, plus a header Roll back all changes. Revert is IDE-side and path-confined (aWriteCommandActionrestores the captured pre-writebeforeTextvia VFS only when inside the project root), refreshes the VFS, and reseeds the binary's read-state (seed_read_state) so its next Edit re-validates against the rolled-back contents. A Revert button also appears on each reviewable transcript row beside View diff. The enumeration + rollback live in a newsession/RollbackManagercollaborator and a purediff/FileRollback(ordering helpers unit-tested) —ClaudeSessionstays a thin delegating orchestrator. (ui/DiffHistoryPanel.kt,ui/ClaudeToolWindowFactory.kt,session/RollbackManager.kt,diff/FileRollback.kt,ui/ChatMessageViews.kt,ui/TranscriptView.kt,ui/ChatPanel.kt.)
- Minimum IDE is now 2025.1 (build 251) —
since-buildmoves up from 243. The composer attach menu uses the fluentFileChooserDescriptorFactory.multiFiles()/singleDir()descriptors introduced in 2025.1; they don't exist on 2024.3, where the old build wouldNoSuchMethodError. 2024.3 users stay on the last compatible release. (build.gradle.kts.) - Attachment mentions are cwd-relative on the wire — a file attachment is sent to the binary as an
@<cwd-relative>mention it actually expands (an absolute@/…path wasn't recognized), while the user bubble shows a clickablejb://openlink to the file (wire text and display text are now built separately). (session/ClaudeSession.kt.) - More file references become links — the markdown linkifier now links bare file paths without a line number too: permissive inside code spans (a
src/Foo.ktin backticks links at line 1), conservative in prose (only an obvious path with a/, or an explicitpath:line, so a product name like "Node.js" isn't turned into a dead link). - Compact attachment chips — smaller chips with a small self-painted ✕ (replacing the chunky stock close button) and a down-scaled file-type glyph. (
ui/AttachmentStripPanel.kt.) - Settings page no longer sprawls — the page is pinned to a fixed content width on the left and its HTML security notes wrap, so on a wide (2K+) monitor the form and the tool-checkbox grids stop stretching edge-to-edge. (
ui/ClaudeSettingsConfigurable.kt.) - Native
/login— no IDE terminal — signing in no longer drops you into a terminal tab (which broke once the Reworked terminal became the default engine in 2025.2: the legacycreateShellWidgetfactory creates a deprecated Classic tab whose command-send races shell startup, soclaude auth loginwas dropped)./loginnow spawnsclaude auth loginunder a real PTY (pty4j, bundled in the platform), lets the binary drive its own OAuth flow, opens the authorize URL in the IDE browser, collects the code from the callback page via a native input dialog, writes it back to the PTY, and restarts the session on success. The pure output parser (URL / "paste code" prompt / result extraction, layout-agnostic to the Ink TUI's cursor positioning) is unit-tested. (process/ClaudeLoginFlow.kt,process/LoginOutputParser.kt,session/ClaudeSession.kt,ui/ChatPanel.kt.)
- IDE terminal launch on 2025.2+ (Reworked terminal default) — the terminal helper, now only the fallback for the native login flow above, drives the Reworked Terminal API (
TerminalToolWindowTabsManager+TerminalView…shouldExecute().send(), available 2025.3+) on modern IDEs and only falls back to the deprecated ClassiccreateShellWidgetpath below 253 — all reflectively, so the verifier sees no deprecated/experimental API. (process/TerminalLauncher.kt.) - Rollback of a file-creating Write deletes the file instead of leaving a 0-byte husk — the snapshot now records whether the file existed before, so reverting a creation removes it while reverting an overwrite restores the prior contents. Revert (per-row and Roll-back-all) also surfaces a success/failure notification so a click is never a silent no-op. (
diff/EditSnapshotStore.kt,diff/FileRollback.kt,session/ClaudeSession.kt.) - Ctrl+O no longer jumps the scroll — collapsing the reasoning blocks used to leave the viewport pointing at shifted content; the view now re-pins after the relayout (and
scrollToBottomvalidates the layout before reading the extent). (ui/TranscriptView.kt.) - The in-card Diff/Revert action buttons on tool rows get a comfortable padded hit area + hover highlight and are spaced apart, so they're no longer tiny adjacent targets. (
ui/ChatMessageViews.kt.)
- Log in from the IDE:
/login(and any auth-failure result /auth_statuserror) can't run inside the TTY-less stream-json session — the binary answers "not available on this environment". The plugin now detects this and offers a "Log in in terminal" notification that opens a native IDE terminal runningclaude auth login, always launched with the binary's absolute path so a GUI IDE that didn't inherit the user's login$PATHstill finds it. Typing/loginin the composer is intercepted client-side and routed to the same flow, and/loginis now listed in the command palette (the binary never advertised it over stream-json). On Windows the command is prefixed with PowerShell's call operator (& "…\claude.exe" auth login) so the quoted path executes instead of being echoed. Uses the bundled Terminal plugin (runtime access via an optional<depends>), guarded so a disabled Terminal plugin degrades to a notice carrying the exact command. (process/TerminalLauncher.kt,session/LoginDetection.kt,session/ClaudeSession.kt,ui/ChatPanel.kt,ui/CommandPalette.kt.)
- Streaming delta coalescing: buffer consecutive assistant/thinking deltas (and the live token usage) on the reader thread and flush them to the EDT in a single
invokeLaterper batch — flushed before every non-delta event / boundary / finalize / result / stop, so ordering is preserved and no delta is lost. Drastically fewer EDT hops/repaints during streaming. (session/ClaudeSession.kt; the shared buffer is lock-guarded so an EDT-side restart can't race the reader thread.) - Per-row transcript render:
TranscriptView.flushDirtyrevalidates/repaints only the changed rows and falls back to a full layout only when a row's preferred height actually changes (ui/TranscriptView.kt). - Markdown memoisation + highlighter cache: skip re-render when the text is unchanged; cache the
SyntaxHighlighterper language; don't highlight unterminated code fences (ui/ChatMessageViews.kt). - O(1) tool-output anchoring: use the existing
byToolUseIdmap instead of anindexOfLastpredicate scan (session/TranscriptModel.kt). - Single shared pulse timer for all tool boxes instead of one
Timerper box (ui/ChatMessageViews.kt/ui/TranscriptView.kt). - Session-scoped quota poll: one poll per session instead of one per open tab (
session/ClaudeSession.kt/ui/ChatPanel.kt). - Lazy transcript restore: reconstruct the last N entries (
DEFAULT_RESTORE_CAP) on restore/fork/open-previous; full context still resumed by the binary (session/SessionTranscriptReader.kt). - O(n) stdout line splitter: read-offset scan + single compaction instead of per-line
delete(process/ClaudeProcess.kt).
- Unbounded stdout buffer: cap a newline-free stream at 16 MiB (drop + warn) so a malformed/stuck binary stream can't grow memory without bound (
process/ClaudeProcess.kt). - Streaming auto-follow: when pinned to the bottom, always re-pin after a flush instead of only when a row's height changed — a stale
preferredSizeno longer stops the transcript from following the stream or clips the newest line (ui/TranscriptView.kt). - Transcript restore cap: drop orphan tool-result rows anywhere in the kept window (not just leading ones), so a restored session never shows a result without its call (
session/SessionTranscriptReader.kt). - Usage meter: a quota poll while the process is stopped no longer overwrites the last good cost/context with null (
session/ClaudeSession.kt). - Delta drain on teardown:
flushDeltasapplies synchronously when already on the EDT (stop/dispose), so final streamed text isn't lost to an unruninvokeLater(session/ClaudeSession.kt). - Markdown memo: bust the cached HTML on IDE theme change so code-block colours follow a LAF switch (
ui/ChatMessageViews.kt). - Defensive handling + tests for duplicate
tool_use_id; removed a redundant render pair in the composer. - Toggling thinking on/off no longer kills the session: switching extended thinking (or the model) restarts the process via
--resume, and the old process's lateonTerminatedcould arrive after the new one was up and tear it down — Claude Code "disappeared" / didn't come back. A per-launch generation counter now ignores stale termination callbacks, so the restart resumes cleanly (session/ClaudeSession.kt). - Diff shows in every permission mode: the pre-write snapshot is now captured on the
tool_useevent (before the binary writes), not only atcan_use_toolapproval — so the inline diff + "View diff" appear in acceptEdits / bypass / auto / dont-ask too, where the binary auto-executes without asking. First-capture-wins so a later re-capture can't overwrite the before-text (session/ClaudeSession.kt,diff/EditSnapshotStore.kt). /loginno longer dead-ends: in the stream-json session the binary has no TTY, so/loginanswered "not available on this environment" and the user was stuck. It's now intercepted and routed to an interactive IDE terminal (see Added), and a detected auth failure surfaces the same actionable prompt instead of just an error line.
- A performance pass plus a post-review hardening pass (thread-safety, behaviour-equivalence, security), and one new user-facing capability (IDE-terminal login). Tests added for the tool index, stream-event parsing, transcript cap (incl. mid-window orphans), delta coalescing, duplicate tool ids, the login detector, and the terminal login command. 503 tests, 0 failures.
- Full SDK protocol surface: all
system/*and stream events parsed (E1) and every host→binary control request wired (E2) —get_settings,get_binary_version,mcp_reconnect/mcp_toggle,stop_task,rename_session, and more. - Graphical session consumption (E7):
SessionUsagePanelpaints context window + honest session-output tokens + a unified quota bar (utilization %, reset countdown and absolute reset hour), replacing the old loose quota labels and the inline token suffix. - Rich IDE attachments (E8):
Attachment/AttachmentEncoder/AttachmentStripPanel/AttachmentActions— pin files/selections as chips (editor actions + 📎 button) and drag&drop/paste images as native base64 content blocks viaControlProtocol.userMessageWithImages; size-guarded and read off the EDT. - Subagent live strip (E10):
SubagentTasksPanel— one card per in-flight Task subagent (tokens/tool-uses/elapsed + Stop), with status/error surfaced;TaskTracker.onUpdatedmerges status/error patches and clears on stop/restart. - Advanced launch options (E6): max turns, max budget (USD), fallback model,
--add-dirroots,--betas, strict MCP config — in Settings and threaded throughSessionLauncher. - Plan mode + richer permissions (E4): ExitPlanMode plan cards, decision reasons, blocked-path context.
- Session management (E5): rename / fork / delete past sessions (binary session files remain the source of truth;
SessionStore.deleteis UUID-guarded). - Native hooks (E3):
hook_callbackanswered host-side by the pureHookBroker(decision + IDE side effects); the real tool gate stayscan_use_tool. - Account & diagnostics (E11/E2-UI): Account, Binary Version, Effective Settings, and an interactive MCP-runtime dialog in the gear menu.
- Diff button on every Edit/Write/MultiEdit row + syntax-highlighted code (E9).
- Tool-call lifecycle on the box: each tool card reflects its state by border colour — sky-blue while in flight, pulsing sky↔amber for a sense of motion while it works, green when finished. The elapsed time is shown while running (the protocol carries no completion %, so time is surfaced instead of a fake progress bar).
- Architecture refactor:
ClaudeSessionandChatPaneldecomposed into single-responsibility collaborators (TokenAccountant, TaskTracker, TranscriptReconciler, DiffLifecycleManager, SessionControlClient, PermissionCardManager, HookBroker, SessionLauncher; UI sub-panels) — the orchestrators are now thin, enabling parallel epic work. - Native UI pass: the consumption readout is rebuilt from native components (
JProgressBar+ labels) and shows the token breakdown (in/cache/out) inline; the "thinking" indicator uses the IDE-nativeAsyncProcessIcon; the Settings page is wrapped in a scroll pane (responsive). The 📎 attach button opens a selector menu (current file / selection / clipboard image) instead of attaching the open file directly. - Authoritative usage: session tokens come from the binary's cumulative
get_session_cost.apiUsage(and context fromget_context_usage) rather than a drifting local fold; quota shows only when the binary reportsutilization(no misleading 0%), retaining the last known value. - Composer model/mode chips clarified: model default → "Default · Opus 4.8 (recommended)"; permission-mode menus/combo show human labels ("Ask each time", "Accept edits", …). Shared
TokenFormat.
- Final hardening pass: image read/encode moved off the EDT (with a pre-read size guard); absolute reset hour + non-colour warning marker restored (WCAG 1.4.1); subagent status/error updates wired; duplicate
formatTokensdivergence removed. - Approving a plan (ExitPlanMode) now flips the plugin's permission mode back to default — the Mode chip no longer stays stuck on "plan".
- 474 tests in the default
testtask (0 failures, 2 Windows-only skips), plus the gated RemoteRobot UI suite (locators validated against a live IDE). Compatible with IU-261 and IU-262 (RC), zero deprecated/internal APIs.
- Headless component tests (IntelliJ Platform
BasePlatformTestCase, run in-process):OpenedDiffsService,ChatSessionManager,SessionHistoryservice round-trip,ClaudeSettingsservice (defaults + always-allow),ClaudeSettingsConfigurable(combo fallbacks + apply/reset/dispose), and real token-accounting verification (all four usage components fold into the session total across messages). - Integration tests driving a real
ClaudeSessionagainstbin/fake-claudewith JSONL fixtures: init/streaming, thinking turn, token accounting, multi-message token fold, rate-limit, tool-use permission resolution, resume reconstruction, interrupt, and the "Write-unsafe context" regression path. - End-to-end UI tests (RemoteRobot, gated by
-PuiTest.enabled=true): chat smoke, View diff, Close All Diffs, jump-to-code, thinking toggle, keyboard shortcuts, Open Previous Session, Settings model combo, notifications — ready to run in the nightly UI workflow. - Branching and release conventions:
docs/BRANCHING.mddocuments the GitFlow + branch-protection conventions. (This entry originally announced arelease.ymland a nightlyui-tests.yml; neither was ever committed. CI/CD landed in 5.0.0 asci.yml,codeql.yml,release.ymlanddrift.yml.) ClaudeSession.handleEventForTest(event)— a@TestOnlyseam so headless tests can drive event reconciliation without spawning the binary.
- Same runtime behaviour as 2.2.0/2.2.1 for end users; this release completes the automated test pyramid (unit → headless → integration → UI) and the maintenance/release workflow. Test count: 239 in the default
testtask (0 failures, 2 Windows-only skips), plus the gated UI suite.
- Maintenance baseline:
SECURITY.md(responsible disclosure policy + SLAs),CONTRIBUTING.md(dev workflow),CODEOWNERS, GitHub issue & PR templates, and Dependabot config for Gradle + the SDK reference (@anthropic-ai/claude-agent-sdk). - CI:
.github/workflows/ci.ymlruns./gradlew test verifyPlugin buildPluginon every push/PR with JDK 21 + Gradle cache and uploads the plugin zip as an artifact. - Drift detection:
.github/workflows/sdk-drift.yml(weekly) opens an issue when a newer SDK is published;.github/workflows/binary-drift.yml(daily) when a newerclaudebinary is released;.github/workflows/binary-probe.yml(weekly + manual) runs the real binary against canonical inputs and opens an issue if it emits an event type the plugin doesn't parse. - Documentation:
docs/RELEASE_PROCEDURE.md,docs/RELEASE_CHECKLIST.md,docs/BINARY_COMPAT.md,docs/FAQ.md,docs/TROUBLESHOOTING.md,docs/TELEMETRY.md— a real release/maintenance workflow for an in-Marketplace plugin. - Test pyramid foundations: new Gradle source sets
integrationTestanduiTest(./gradlew integrationTestruns against a deterministicbin/fake-claudePython stand-in fed JSONL fixtures;uiTestreserved for the Sprint 3 RemoteRobot end-to-end suite, gated by-PuiTest.enabled=true). - Coverage:
kotlinx-koverintegrated;./gradlew koverHtmlReportproduces a coverage report. - Layer A unit tests (67 new, total 202 / 0 fail / 2 skipped on non-Windows):
DiffPresenter.isWithinRootdirect (incl. symlink escape attempts), exhaustivePermissionBrokermatrix (mode × tool × within-root × remembered),ClaudeBinaryLocator(incl. Windows.cmdshim regression resolved withAssumptions.assumeTrue),McpConfigBuilder(SSE / streamable-http / stdio + custom server merging + invalid JSON tolerance),Protocol.parseAskQuestions, andMarkdownRendereredge combinations (table cells with code/links, unterminated fences, nested task lists, contiguous autolink +path:line). bin/fake-claudePython stand-in plus theinit_basic.jsonlfixture: handles the initialize handshake, replays a streamed text turn withmessage_start/content_block_delta/message_delta/result, and emits per-message usage with all four token components so integration tests can pin token-accounting behaviour without hitting the real model.
- README install path now points to the JetBrains Marketplace as the canonical source (GitHub remains the source of truth for code).
- 2.2.1 has the same runtime behaviour as 2.2.0 — this release is the infrastructure update (tests + workflows + docs) so the plugin can be maintained seriously with real users on the Marketplace.
- Model picker reflects what the binary actually returns — the Settings combo now lists the binary's modern aliases (
default= Opus 4.8 with 1M context,sonnet= Sonnet 4.6,haiku= Haiku 4.5) and updates live as soon as theinitializehandshake lands, showing each as itsdisplayName("Default (recommended)", "Sonnet", "Haiku") instead of the raw wire value. The historical Opus 4.7/4.5/Opusplan tags stay as fallback for back-compat. - Path:line links inside code spans —
`src/Foo.kt:42`(the natural way the model writes references) now renders as a clickablejb://openlink wrapped in<code>instead of inert monospace text. Project-confinement (DiffPresenter.isWithinRoot) still gates the click. - Protocol surface bumped to SDK 0.3.161 / binary 2.1.161:
ModelInfocarriessupportsEffort/supportedEffortLevels/supportsAdaptiveThinking/supportsFastMode/supportsAutoMode;AccountInfocarriesapiProvider/apiKeySource. Extrasystem/*events from the new binary (task_progress,task_notification,background_task_*,auth_status,session_state_changed) are tolerated by the lenient codec — UI surfacing to come. - Tests:
MarkdownRendererlinkify-inside-code-span (134 total).
- Default model is now
default(the binary's recommended-tier alias), not the hard-codedclaude-opus-4-7. Fresh installs follow the binary's recommendation (currently Opus 4.8); existing settings keep their persisted model value untouched.
- Marketplace publishing: migrated the bundled MCP plugin lookup from the internal
PluginManager.getInstance().findEnabledPlugin(PluginId)to the public staticPluginManager.getPlugin(PluginId)?.takeIf { it.isEnabled }. This was the lone internal-API hit that blocked the 2.0.1 upload re-check; verified withjavapagainst the platform jars.
- Persistent diff from the transcript — Edit/Write/MultiEdit tool cards carry a "View diff" button that re-opens the old↔new diff at any time, in any permission mode. A new
EditSnapshotStorecaptures the pre-write file contents at approval time, keyed bytool_use_id. - Hunk-by-hunk acceptance — the permission card lists the change's hunks (via the platform diff
ComparisonManager) with checkboxes; accepting a subset sends a narrowedupdatedInputso the binary writes only the selected hunks.file_pathis never modified. - AskUserQuestion options wrap — labels, descriptions and the per-option
preview(previously unused) render in full instead of clipping to one line. - "Explain with Claude" editor-popup action sends the current selection (with file path) to the active session.
- Jump-to-code —
path:linereferences in replies becomejb://openlinks that navigate to the file/line in the IDE. - "Always allow" per tool — persisted in
ClaudeSettings; remembered tools auto-approve while reviewable writes stay confined to the project root. Settings ▸ Claude Code now lists the remembered tools with a Remove action, so the rule can be revoked without editing XML. - Session attention notifications + tab badge — a background session with a pending permission, a finished turn, or an error raises a notification and badges its tab; suppressed when that tab is the one on screen.
- Session history (reads the binary's own files) — past conversations are read back from the
claudebinary's session transcripts (~/.claude/projects/.../<sessionId>.jsonl), the single source of truth. "Open Previous Session…" lists the project's sessions by their real title (as--resumeshows them) and re-attaches via--resume. The plugin persists no transcripts — only the open-tab session ids, inworkspace.xml(not committed by convention). - Restore on startup — the tabs you had open are reopened automatically; if none were recorded, the most recent session is restored. Toggle: Settings ▸ Claude Code ▸ "Restore open chats on startup".
- Markdown rendering: strikethrough (
~~), GFM task-list checkboxes, nested lists. - Tests:
EditSnapshotStore,PermissionBrokertool_use_id plumbing,HunkSelection,MarkdownRenderer,SessionHistoryopen-tab ids,SessionStorepath-traversal guard + cwd encoding,SessionTitleReader/SessionTranscriptReaderJSONL parsing, and the settings enums (132 total).
- Permission mode, effort and MCP transport are now backed by typed enums (
PermissionMode/EffortLevel/McpTransport) as the single source of truth for allowed values and branching; the persisted/wire strings are unchanged (no config migration).
- Jump-to-code navigation is confined to the project root (
DiffPresenter.isWithinRoot): a craftedjb://openlink cannot open absolute paths,~/.ssh,/etc, or..-traversed files. - Explicit Markdown links are restricted to an allow-list of schemes (
http/https/jb) with the href quote-escaped; other schemes (javascript:,file:,data:, relative) render as plain text. - No conversation content is written to project files anymore: session history keeps only open-tab ids in
workspace.xml. Session-file reads are confined to~/.claude/projectsand gated by a UUID-shaped id check (SessionStore), so a crafted session id can't traverse out of the tree.
- Markdown: a bare URL inside an explicit link's href is no longer double-linkified (
<a href="<a href=…">). - Notifications no longer pop for the chat already on screen (the over-strict tool-window
isActivecheck is gone; visible+selected tab is enough to suppress), and the notification's Open action now dismisses it. - Fixed a "Write-unsafe context!" crash when refreshing files the agent edited: the VFS refresh is now asynchronous (
refreshIoFiles), which is safe from the non-write-safe modality it runs under. - Extended thinking shows again on current models (Opus 4.7 /
claude2.1.152+): reasoning is now enabled via the launch flags--thinking adaptive --thinking-display summarizedinstead of the deprecatedset_max_thinking_tokenscontrol, which no longer surfaces "Thought process" blocks. Thinking is now on/off (adaptive — the model decides depth); toggling the chip restarts the session via--resume.
- Extended the supported IDE range to the current EAP (
until-build=262.*); verified Compatible against IU-262. - Replaced the internal
PluginManagerCorelookup for the bundled MCP Server plugin with the publicPluginManagerby-id API, removing the last internal-API usage.
- Auto-approved file writes in
acceptEdits/bypassPermissionsare confined to the project root: a write whose canonical path (symlinks resolved) falls outside the project degrades to a manual Accept/Reject card instead of being written silently. - Trust-on-open gate: when a project-level
claude-code.xmlcarries a source script or a custom stdio MCP server — both of which execute code at launch — the plugin prompts for confirmation once before running them (declining aborts the launch). - The source script is invoked with its path as a positional shell argument instead of being interpolated into the command string, removing a shell-injection vector via a crafted path.
- Settings now warn that environment variables are stored in plain text in
claude-code.xmland that the source script is executed on session start.
- EDT freeze on session start: environment resolution (sources a login shell, multi-second timeout) and process spawn now run on a pooled thread; the resolved environment is cached per session. Opening the first chat or sending the first prompt no longer hangs the IDE.
- In-flight control requests are now completed (with failure) on
stop()/ process termination / dispose, fixing dialogs stuck on "Loading…" and leaked callbacks. - Control requests now have a 30s watchdog; a hung binary no longer leaves the callback pending indefinitely.
- Process start failures are surfaced via notification (not just the transcript) and no longer leave a half-initialized "ready" session;
writeLinelogs (and reports) lines dropped to a dead stdin instead of discarding them silently.
- First unit-test suite (80 tests):
ProtocolParser,ControlProtocol,DiffPresenterreconstruction,TranscriptModelhierarchy,RateLimitInfomath, and environment parsing (EnvScriptLoader.parse,ClaudeSettings.parseEnv).
- MCP config building extracted to a standalone, testable
McpConfigBuilder(identical wire output). - Thread-safe tab counter (
AtomicInteger); named constants for UI timings/quota thresholds; debug logging on previously silent decode/parse failures.
- IDE tools over MCP (opt-in). Two independent controls in Settings ▸ Claude Code:
- Enable JetBrains MCP server — wires JetBrains' own MCP Server plugin via
--mcp-config. Pick the transport (sse,streamable-http, orstdio) and port; forsse/streamable-httpthe default localhost endpoint is synthesized (no JSON to type), andstdiois built automatically from the running IDE's paths (JBRjava+ the bundledmcpserverlibs), so it works on Windows unchanged. - Custom MCP servers — add any number of your own servers as a JSON object (
name → server config), merged alongside the JetBrains one.
- Enable JetBrains MCP server — wires JetBrains' own MCP Server plugin via
- Off by default; tool calls remain gated by the in-chat permission prompt. Invalid custom JSON is rejected on save.
- Settings: the model dropdown was empty when opened before the binary's initialize handshake — it now always lists the available models plus known fallbacks (shared with the gear menu).
- Settings: removed the blank entry in the Effort dropdown.
- Default model is now Opus 4.7 (
claude-opus-4-7). - Default effort is now medium.
- Windows support: the
claudebinary is detected on Windows (claude.exe/claude.cmd) across npm, scoop, volta, chocolatey and~\.local\bin. npm.cmdshims are driven asnode cli.jsdirectly, bypassing cmd.exe (which corrupted the streaming stdio pipe and mangled argument quoting). - Settings: explicit overrides for the
claudeandnodeexecutable paths — the catch-all for non-standard installs, version managers, or a GUI IDE that doesn't inherit the user's PATH. - Settings: configurable environment variables (
KEY=VALUEper line), injected into the binary's process — useful on Windows forPATHadditions. - Settings: Source script — point to a
.sh(sourced in the login shell on Linux/macOS) or a PowerShell profile/.ps1(dot-sourced on Windows); the resulting environment is captured and applied to theclaudeprocess, so the IDE inherits the samePATH/setup as the user's own shell. - "Binary not found" notification now carries a Configure paths… action that opens the settings page directly.
- Auto-detected
claudepath is persisted to settings on first successful launch (and refreshed if a saved path goes stale), so launches are stable and the path is visible/editable.
- Tool output is now shown in the chat as a code block immediately below the tool call card. Outputs longer than 200 lines are truncated with an indicator. Supports all tools (Bash, Read, Edit, Grep, Glob, WebFetch, etc.)
- Tool calls are now collapsible groups: a disclosure triangle on each tool card shows/hides its output. Applies to every tool that produces output.
- Subagent (
Task/Agent) activity nests under its Agent: the subagent's tool calls, outputs and text are anchored and indented beneath the Agent card, and collapse hierarchically (collapsing the Agent hides its whole subtree; collapsing a sub-tool hides only its output).
- Info bar above the composer reordered: (1) Resets in countdown, (2) Reset Hour, (3) Session Usage %, (4) Brewing / live tokens / Esc to interrupt
- Tool outputs now anchor directly under their tool call instead of drifting to the end of the transcript — including tools that require human interaction (permission cards,
AskUserQuestion) and long-running calls. Parallel tool calls keep each output under its own call. - Replaced all deprecated
JBUI.scale()calls withJBUIScale.scale()across the UI (ChatPanel,TranscriptView,ChatMessageViews,CommandPalette,ClaudeSettingsConfigurable,ChatTheme)
- Quota bar stays visible with reset countdown when utilization % is not reported (Max plans); % meter hides independently
isWarning/isExhaustedno longer fire onoverageStatus = "rejected"alone- Token counter now accumulates correctly across multi-message turns (tool calls, chained assistant messages)
- Failed turns with no
resulttext (error_*subtypes) surface theerrorslist or subtype name — no more silent failures dispose()sends EOF before killing the process (clean exit, same order asstop())LiveUsageupdates moved to EDT to eliminate read-modify-write race on token countersreadyandprocessmarked@Volatile— visibility gap on session start/stop across threads- Startup queue flushed after
system/init— messages sent before the handshake are no longer dropped JBUI.scale→JBUIScale.scalefor correct stroke scaling on IntelliJ Platform 2025+
errors: List<String>field onResultMessageto capture SDKSDKResultError.errorspayloads
- Native stream-json + control protocol transport (one long-lived process per tab)
- Streaming chat transcript with markdown rendering (bold, code blocks, tables)
- Multi-chat tabs via
ChatSessionManager - Permission-gated diff review: Edit/Write proposals shown as in-editor diff tab + inline Accept/Reject card
AskUserQuestionsupport with multi-select option cards- Slash-command palette (all commands from
initialize+ client-side/btw) - Model / effort / permission-mode / thinking chips + gear menu
- Multi-prompt queue (send follow-ups while agent works)
- Quota bar + live token counter + reset countdown
- Auto-diff on acceptEdits / bypass permission mode
- Ctrl+O toggle for reasoning blocks
- Status bar with thinking indicator, live token count and "Esc to interrupt"
- Settings: model, permission mode, effort, thinking tokens, allowed/disallowed tools, setting sources, output style
- UI rethemed to follow the active IDE theme (light/dark); Claude logo icon