From 315f9b9785d699f08feb8c51786c71bb86ecd219 Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 15:55:55 +0200 Subject: [PATCH 1/8] chore(release): bump version to 5.7.1 The release workflow reads the version from build.gradle.kts, and the README states it twice as user-facing text: the badge and the name of the artifact buildPlugin writes. All three move together or the README describes a zip that no build produces. --- README.md | 4 ++-- build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 15ab36b1..0ac31b26 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Claude Code Native -[![Version](https://img.shields.io/badge/version-5.7.0-E07B5A)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-5.7.1-E07B5A)](CHANGELOG.md) [![IDE](https://img.shields.io/badge/JetBrains-2025.3.1%20%E2%86%92%20263.*-000000?logo=jetbrains)](#requirements) [![Marketplace](https://img.shields.io/badge/Marketplace-Claude%20Code%20Native-2A2A2A)](https://plugins.jetbrains.com/plugin/31965-claude-code-native) [![License](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE) @@ -542,7 +542,7 @@ wrapper is included. ```bash JAVA_HOME=/path/to/a/jdk-21 ./gradlew buildPlugin -# → build/distributions/claude-code-native-5.7.0.zip +# → build/distributions/claude-code-native-5.7.1.zip ``` Install it with **Settings ▸ Plugins ▸ ⚙ ▸ Install Plugin from Disk**. diff --git a/build.gradle.kts b/build.gradle.kts index 3f14fcf5..70c33758 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ plugins { } group = "dev.lain" -version = "5.7.0" +version = "5.7.1" repositories { mavenCentral() From 042ce198419320d85e84f4173546f3c6c36a6c62 Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 16:29:40 +0200 Subject: [PATCH 2/8] chore(release): carry the version bump into the identity constant The bump missed a fourth place the version is written. PluginIdentity is the one the plugin announces on every outbound request, and the descriptor cannot be read at runtime without an internal API this build refuses to ship, so the constant is hand-kept and PluginIdentityTest is what keeps it honest. It went red on 5.7.0 against a 5.7.1 build, which is the test doing its job. --- src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt index cdc4d732..7d456da8 100644 --- a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt +++ b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt @@ -2,7 +2,7 @@ package dev.lain.claudejb.util object PluginIdentity { - const val PLUGIN_VERSION = "5.7.0" + const val PLUGIN_VERSION = "5.7.1" private const val PROJECT_URL = "https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains" From 184689623c48a2d6bc9eca26c83547538b84c62e Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 16:30:17 +0200 Subject: [PATCH 3/8] feat(session): drive a chat from claude.ai with Remote Control The slash command is closed in the mode this plugin launches, and answers "isn't available in this environment". The control request behind it is not: the binary takes a remote_control subtype carrying an enabled flag, and runs the bridge itself with the session credential. That is the same shape as the btw command, which is a side_question request rather than the command it looks like. The reply rides SessionControlClient.send instead of SessionQueries.ask, because a success comes back with no payload at all. Ask decodes the payload whether the request succeeded or failed, so a toggle built on it would paint itself on after a refusal and never say so. Two ways in, one path: the composer's phone button and the settings menu row both emit the same settingsToggle key, so there is one destination to keep honest rather than two that can disagree. --- .../lain/claudejb/session/ClaudeSession.kt | 26 +++++++++++ .../dev/lain/claudejb/session/ControlAsks.kt | 18 ++++++++ .../lain/claudejb/session/SessionQueries.kt | 11 +++++ .../dev/lain/claudejb/ui/ChatBridgeRouter.kt | 4 ++ .../lain/claudejb/ui/jcef/JcefSettingsMenu.kt | 11 +++++ .../dev/lain/claudejb/ui/jcef/JcefState.kt | 1 + src/main/resources/jcef/app-composer.js | 45 ++++++++++++++++++- .../claudejb/ui/jcef/JcefSettingsMenuTest.kt | 5 ++- 8 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt index 90fceb18..e98a6d33 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt @@ -313,6 +313,8 @@ class ClaudeSession( private set var account: AccountInfo = AccountInfo() private set + var remoteControlEnabled: Boolean = false + private set @Volatile private var process: ClaudeProcess? = null @@ -768,6 +770,30 @@ class ClaudeSession( } } + fun setRemoteControl(enabled: Boolean, onSettled: () -> Unit) { + queries.setRemoteControl(enabled) { outcome -> + if (outcome.ok) { + remoteControlEnabled = outcome.enabled + fireState() + } + transcript.add(Speaker.SYSTEM, remoteControlNotice(outcome)) + onSettled() + } + } + + private fun remoteControlNotice(outcome: RemoteControlOutcome): String = when { + !outcome.ok -> { + val what = if (outcome.enabled) "switched on" else "switched off" + "Remote Control could not be $what" + (outcome.error?.let { ": $it" } ?: ".") + } + + !outcome.enabled -> "Remote Control is off. This chat keeps running in the IDE." + + outcome.sessionUrl != null -> "Remote Control is on — ${outcome.sessionUrl}" + + else -> "Remote Control is on. The session is listed at https://claude.ai/code" + } + fun removeQueued(index: Int) = edt { if (index in queue.indices) { val copy = queue.toMutableList() diff --git a/src/main/kotlin/dev/lain/claudejb/session/ControlAsks.kt b/src/main/kotlin/dev/lain/claudejb/session/ControlAsks.kt index 01dbe8ee..fd017bfc 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/ControlAsks.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/ControlAsks.kt @@ -19,6 +19,18 @@ import kotlinx.serialization.json.put data class RewindResult(val canRewind: Boolean, val error: String?, val filesChanged: List) +data class RemoteControlOutcome( + val enabled: Boolean, + val ok: Boolean, + val sessionUrl: String?, + val error: String?, +) + +private val CLAUDE_SESSION_URL = Regex("""https://claude\.ai/[A-Za-z0-9./_#?=&-]+""") + +internal fun sessionUrlIn(payload: JsonObject?): String? = + payload?.let { CLAUDE_SESSION_URL.find(it.toString())?.value } + @Serializable data class PlanInfo( val exists: Boolean? = null, @@ -144,6 +156,12 @@ object Asks { }, ) + fun remoteControl(enabled: Boolean) = Ask( + subtype = "remote_control", + params = { put("enabled", enabled) }, + decode = { it }, + ) + fun rewind(userMessageId: String, dryRun: Boolean) = Ask( subtype = "rewind_files", params = { diff --git a/src/main/kotlin/dev/lain/claudejb/session/SessionQueries.kt b/src/main/kotlin/dev/lain/claudejb/session/SessionQueries.kt index 6cf83c25..ab9017a0 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/SessionQueries.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/SessionQueries.kt @@ -46,6 +46,17 @@ class SessionQueries( fun requestRewindFiles(userMessageId: String, dryRun: Boolean, onResult: (RewindResult?) -> Unit) = ask(Asks.rewind(userMessageId, dryRun), onResult) + fun setRemoteControl(enabled: Boolean, onResult: (RemoteControlOutcome) -> Unit) { + val ask = Asks.remoteControl(enabled) + if (!isRunning()) { + edt { onResult(RemoteControlOutcome(enabled, ok = false, sessionUrl = null, error = "the session is not running")) } + return + } + controlClient.send({ id -> ControlProtocol.of(id, ask.subtype, ask.params) }) { res -> + edt { onResult(RemoteControlOutcome(enabled, res.success, sessionUrlIn(res.payload), res.error)) } + } + } + fun reconnectMcp(name: String) = fireAndForget { id -> ControlProtocol.mcpReconnectRequest(id, name) } fun toggleMcp(name: String, enabled: Boolean) = fireAndForget { id -> ControlProtocol.mcpToggleRequest(id, name, enabled) } diff --git a/src/main/kotlin/dev/lain/claudejb/ui/ChatBridgeRouter.kt b/src/main/kotlin/dev/lain/claudejb/ui/ChatBridgeRouter.kt index 274f8603..c7eb9e84 100644 --- a/src/main/kotlin/dev/lain/claudejb/ui/ChatBridgeRouter.kt +++ b/src/main/kotlin/dev/lain/claudejb/ui/ChatBridgeRouter.kt @@ -127,6 +127,10 @@ internal class ChatBridgeRouter(private val panel: JcefChatPanel) { if (!m.on) session.guardApprovals.revoke(rule, command) return true } + if (JcefSettingsMenu.isRemoteControl(m.key)) { + session.setRemoteControl(m.on) { JcefChatPanel.pushSettingsMenuToAll() } + return true + } val models = session.models.map { it.value } var known = false settings.update { known = JcefSettingsMenu.apply(settings.scope.id, it, m.key, m.on, models) } diff --git a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt index 98faa888..a683013b 100644 --- a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt +++ b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt @@ -24,6 +24,7 @@ internal object JcefSettingsMenu { val effort: String?, val mode: String, val approvals: Map> = emptyMap(), + val remoteControl: Boolean = false, ) fun json(scope: String, state: ClaudeSettings.State, session: ClaudeSession): JsonArray = @@ -33,6 +34,7 @@ internal object JcefSettingsMenu { modelRows(selected) effortRows(selected) modeRows(selected) + remoteControlRows(selected) chatRows(state) securityRows(scope, state) sessionApprovalRows(selected.approvals) @@ -62,6 +64,8 @@ internal object JcefSettingsMenu { } } + fun isRemoteControl(key: String): Boolean = key == REMOTE_CONTROL + fun alwaysAllowTool(key: String): String? { if (!key.startsWith("$ALWAYS:")) return null return key.removePrefix("$ALWAYS:").takeIf { it in ToolNaming.BUILTIN_TOOLS } @@ -87,6 +91,10 @@ internal object JcefSettingsMenu { } } + private fun JsonArrayBuilder.remoteControlRows(selected: Selected) { + entry(REMOTE_CONTROL, "Remote control", "Drive this chat from claude.ai", selected.remoteControl) + } + private fun JsonArrayBuilder.chatRows(s: ClaudeSettings.State) { entry("restoreChats", "Chat", "Restore open chats on startup", s.restoreOpenChatsOnStartup) entry("reduceMotion", "Chat", "Reduce motion", s.reduceMotion) @@ -279,8 +287,11 @@ internal object JcefSettingsMenu { effort = session.effort, mode = session.permissionMode, approvals = session.guardApprovals.all(), + remoteControl = session.remoteControlEnabled, ) + internal const val REMOTE_CONTROL = "remoteControl" + private const val APPROVAL = "approval" private const val GUARD_MODE = "guardmode" private const val MODEL = "model" diff --git a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt index 25a7bf78..9e8e73d2 100644 --- a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt +++ b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt @@ -61,6 +61,7 @@ object JcefState { } put("guardOn", session.guardEnforced) + put("remoteControlOn", session.remoteControlEnabled) put("provider", JcefComposerOptions.providerJson(provider)) put("model", JcefComposerOptions.modelJson(session)) diff --git a/src/main/resources/jcef/app-composer.js b/src/main/resources/jcef/app-composer.js index 0c92c9b7..a8ec85fb 100644 --- a/src/main/resources/jcef/app-composer.js +++ b/src/main/resources/jcef/app-composer.js @@ -15,6 +15,8 @@ var followBtnRef = null; var guardOn = true; var guardBtnRef = null; + var rcOn = false; + var rcBtnRef = null; function applyFollow() { if (followBtnRef) { @@ -67,6 +69,30 @@ applyGuard(); }; + function phoneGlyph() { + return ( + '' + ); + } + + function applyRemoteControl() { + if (!rcBtnRef) return; + if (rcOn) rcBtnRef.classList.add('active'); + else rcBtnRef.classList.remove('active'); + rcBtnRef.title = rcOn + ? 'Remote Control is on — click to disconnect this chat from claude.ai' + : 'Remote Control is off — click to drive this chat from claude.ai'; + } + + CX.setRemoteControlOn = function (on) { + var next = on === true; + if (next === rcOn) return; + rcOn = next; + applyRemoteControl(); + }; + function ensureBuilt() { if (built) return true; if (!CC.els || !CC.els.composer) return false; @@ -163,7 +189,23 @@ }); guardBtn.innerHTML = guardGlyph(); guardBtnRef = guardBtn; - var barRight = h('div', { class: 'bar-right' }, guardBtn, followBtn, vibeBtn, sendBtn); + + var rcBtn = h('button', { + class: 'bar-icon', + attrs: { type: 'button', 'aria-label': 'Remote Control' }, + on: { + click: function (e) { + e.preventDefault(); + e.stopPropagation(); + send({ type: 'settingsToggle', key: 'remoteControl', on: !rcOn }); + }, + }, + }); + rcBtn.innerHTML = phoneGlyph(); + rcBtnRef = rcBtn; + applyRemoteControl(); + + var barRight = h('div', { class: 'bar-right' }, rcBtn, guardBtn, followBtn, vibeBtn, sendBtn); var bar = h('div', { class: 'composer-bar' }, barLeft, barRight); var guardMenu = CC.durationMenu({ @@ -441,6 +483,7 @@ if (!s) return; announceTurnState(s); CX.setGuardOn(s.guardOn); + CX.setRemoteControlOn(s.remoteControlOn); CX.renderAuth(s); renderSendMode(s); CX.renderPills(s); diff --git a/src/test/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenuTest.kt b/src/test/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenuTest.kt index e307680c..c5ee809f 100644 --- a/src/test/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenuTest.kt +++ b/src/test/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenuTest.kt @@ -46,7 +46,7 @@ class JcefSettingsMenuTest { fun `the groups are drawn in the declared order`() { assertEquals( listOf( - "Model", "Effort", "Permission mode", "Chat", "Guard mode", "Security", + "Model", "Effort", "Permission mode", "Remote control", "Chat", "Guard mode", "Security", "Setting sources", "Allowed tools", "Disallowed tools", "Always allowed tools", "MCP", ), menu().map { it.str("group") }.distinct(), @@ -63,7 +63,7 @@ class JcefSettingsMenuTest { assertTrue(rows.none { it.bool("deferred") }, "$group takes effect on the live session") } - listOf("Chat", "Security", "Always allowed tools").forEach { group -> + listOf("Chat", "Remote control", "Security", "Always allowed tools").forEach { group -> val rows = byGroup.getValue(group) assertTrue(rows.all { it.str("type") == "check" }, "$group must be a checkbox group") assertTrue(rows.none { it.bool("deferred") }, "$group takes effect immediately") @@ -115,6 +115,7 @@ class JcefSettingsMenuTest { menu().forEach { row -> val key = row.str("key") val accepted = JcefSettingsMenu.alwaysAllowTool(key) != null || + JcefSettingsMenu.isRemoteControl(key) || write(ClaudeSettings.State(), key, true) assertTrue(accepted, "no destination accepts the menu key '$key'") } From ab54493c57e89c1f2b485f08943e5071998cb473 Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 16:44:56 +0200 Subject: [PATCH 4/8] fix(jcef): let a refused Remote Control toggle say so, and stay off The binary answers the enable request with an error when the account or the organisation has Remote Control switched off, so the host state was already right. What lied was the page. The settings row flipped itself on the click, before the host had answered. That is correct for every other row in this menu, because they write to the settings document and cannot be refused; this is the first one whose destination can say no. Rows may now declare themselves host-owned, and the page leaves those alone until the state it is sent says otherwise. The composer button had no way to fail. It stayed off after a refusal with the reason reachable only by reading the transcript, which is the same as failing silently for anyone who was watching the button they just pressed. The refusal now travels to the page and the button wears it. --- .../lain/claudejb/session/ClaudeSession.kt | 20 +++++++++++-------- .../lain/claudejb/ui/jcef/JcefSettingsMenu.kt | 10 +++++++++- .../dev/lain/claudejb/ui/jcef/JcefState.kt | 1 + .../resources/jcef/app-composer-settings.js | 4 ++++ src/main/resources/jcef/app-composer.js | 19 ++++++++++-------- src/main/resources/jcef/css/composer.css | 3 +++ 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt index e98a6d33..d9345d4e 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt @@ -315,6 +315,8 @@ class ClaudeSession( private set var remoteControlEnabled: Boolean = false private set + var remoteControlError: String? = null + private set @Volatile private var process: ClaudeProcess? = null @@ -772,20 +774,22 @@ class ClaudeSession( fun setRemoteControl(enabled: Boolean, onSettled: () -> Unit) { queries.setRemoteControl(enabled) { outcome -> - if (outcome.ok) { - remoteControlEnabled = outcome.enabled - fireState() - } + if (outcome.ok) remoteControlEnabled = outcome.enabled + remoteControlError = if (outcome.ok) null else remoteControlRefusal(outcome) + fireState() transcript.add(Speaker.SYSTEM, remoteControlNotice(outcome)) onSettled() } } + private fun remoteControlRefusal(outcome: RemoteControlOutcome): String { + val what = if (outcome.enabled) "switch Remote Control on" else "switch Remote Control off" + return outcome.error?.takeIf { it.isNotBlank() }?.let { "Could not $what: $it" } ?: "Could not $what." + } + private fun remoteControlNotice(outcome: RemoteControlOutcome): String = when { - !outcome.ok -> { - val what = if (outcome.enabled) "switched on" else "switched off" - "Remote Control could not be $what" + (outcome.error?.let { ": $it" } ?: ".") - } + !outcome.ok -> remoteControlRefusal(outcome) + + " Remote Control has to be enabled for your account, and by your organisation on Team and Enterprise plans." !outcome.enabled -> "Remote Control is off. This chat keeps running in the IDE." diff --git a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt index a683013b..d78da930 100644 --- a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt +++ b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefSettingsMenu.kt @@ -92,7 +92,13 @@ internal object JcefSettingsMenu { } private fun JsonArrayBuilder.remoteControlRows(selected: Selected) { - entry(REMOTE_CONTROL, "Remote control", "Drive this chat from claude.ai", selected.remoteControl) + entry( + REMOTE_CONTROL, + "Remote control", + "Drive this chat from claude.ai", + selected.remoteControl, + hostOwned = true, + ) } private fun JsonArrayBuilder.chatRows(s: ClaudeSettings.State) { @@ -165,6 +171,7 @@ internal object JcefSettingsMenu { radio: Boolean = false, deferred: Boolean = false, sub: String? = null, + hostOwned: Boolean = false, ) = addJsonObject { put("key", key) put("group", group) @@ -173,6 +180,7 @@ internal object JcefSettingsMenu { put("on", on) put("type", if (radio) TYPE_RADIO else TYPE_CHECK) put("deferred", deferred) + if (hostOwned) put("hostOwned", true) } private val FLAG_SETTERS: Map Unit> = mapOf( diff --git a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt index 9e8e73d2..81779640 100644 --- a/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt +++ b/src/main/kotlin/dev/lain/claudejb/ui/jcef/JcefState.kt @@ -62,6 +62,7 @@ object JcefState { put("guardOn", session.guardEnforced) put("remoteControlOn", session.remoteControlEnabled) + put("remoteControlError", session.remoteControlError) put("provider", JcefComposerOptions.providerJson(provider)) put("model", JcefComposerOptions.modelJson(session)) diff --git a/src/main/resources/jcef/app-composer-settings.js b/src/main/resources/jcef/app-composer-settings.js index 46ec4f5f..65b3887b 100644 --- a/src/main/resources/jcef/app-composer-settings.js +++ b/src/main/resources/jcef/app-composer-settings.js @@ -177,6 +177,10 @@ if (CC.announce) CC.announce(labelOf(it) + ' selected'); return; } + if (it.hostOwned) { + CC.send({ type: 'settingsToggle', key: String(it.key), on: !already }); + return; + } it.on = !already; applyState(row, !already); CC.send({ type: 'settingsToggle', key: String(it.key), on: !already }); diff --git a/src/main/resources/jcef/app-composer.js b/src/main/resources/jcef/app-composer.js index a8ec85fb..c54191f5 100644 --- a/src/main/resources/jcef/app-composer.js +++ b/src/main/resources/jcef/app-composer.js @@ -16,6 +16,7 @@ var guardOn = true; var guardBtnRef = null; var rcOn = false; + var rcError = null; var rcBtnRef = null; function applyFollow() { @@ -79,17 +80,19 @@ function applyRemoteControl() { if (!rcBtnRef) return; - if (rcOn) rcBtnRef.classList.add('active'); - else rcBtnRef.classList.remove('active'); - rcBtnRef.title = rcOn - ? 'Remote Control is on — click to disconnect this chat from claude.ai' - : 'Remote Control is off — click to drive this chat from claude.ai'; + rcBtnRef.classList.toggle('active', rcOn); + rcBtnRef.classList.toggle('failed', !rcOn && !!rcError); + if (!rcOn && rcError) rcBtnRef.title = rcError; + else if (rcOn) rcBtnRef.title = 'Remote Control is on — click to disconnect this chat from claude.ai'; + else rcBtnRef.title = 'Remote Control is off — click to drive this chat from claude.ai'; } - CX.setRemoteControlOn = function (on) { + CX.setRemoteControlOn = function (on, error) { var next = on === true; - if (next === rcOn) return; + var nextError = typeof error === 'string' && error ? error : null; + if (next === rcOn && nextError === rcError) return; rcOn = next; + rcError = nextError; applyRemoteControl(); }; @@ -483,7 +486,7 @@ if (!s) return; announceTurnState(s); CX.setGuardOn(s.guardOn); - CX.setRemoteControlOn(s.remoteControlOn); + CX.setRemoteControlOn(s.remoteControlOn, s.remoteControlError); CX.renderAuth(s); renderSendMode(s); CX.renderPills(s); diff --git a/src/main/resources/jcef/css/composer.css b/src/main/resources/jcef/css/composer.css index ad39fbaa..344086ef 100644 --- a/src/main/resources/jcef/css/composer.css +++ b/src/main/resources/jcef/css/composer.css @@ -294,6 +294,9 @@ .bar-icon.active { color: var(--accent); } +.bar-icon.failed { + color: var(--danger); +} .bar-icon .vibe-emoji { font-size: 16px; line-height: 1; From 1c57b91259a4e419f768262fe741ef7031c34e54 Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 16:59:56 +0200 Subject: [PATCH 5/8] chore(release): 5.8.0 Remote Control is a new capability, not a repair, so the release is a minor. The number is written in four places and the release workflow greps only the first of them, which is why the other three are moved in the same commit. The changelog section is the one the release job extracts for the GitHub Release, and the release notes are what the Marketplace shows as What's New. Both name what the feature needs to work, because the refusal a user is most likely to meet is an account or organisation that has Remote Control off. --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 4 ++-- RELEASE_NOTES.md | 15 +++++++++++++++ build.gradle.kts | 2 +- .../dev/lain/claudejb/util/PluginIdentity.kt | 2 +- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f31a5864..6035e951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.8.0] — 2026-08-28 + +**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. + +### Added +- **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. + ## [5.7.0] — 2026-08-21 **The guard is now something you can see, tune and audit**, instead of a set of rules that only spoke up to diff --git a/README.md b/README.md index 0ac31b26..df28ecc6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Claude Code Native -[![Version](https://img.shields.io/badge/version-5.7.1-E07B5A)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-5.8.0-E07B5A)](CHANGELOG.md) [![IDE](https://img.shields.io/badge/JetBrains-2025.3.1%20%E2%86%92%20263.*-000000?logo=jetbrains)](#requirements) [![Marketplace](https://img.shields.io/badge/Marketplace-Claude%20Code%20Native-2A2A2A)](https://plugins.jetbrains.com/plugin/31965-claude-code-native) [![License](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE) @@ -542,7 +542,7 @@ wrapper is included. ```bash JAVA_HOME=/path/to/a/jdk-21 ./gradlew buildPlugin -# → build/distributions/claude-code-native-5.7.1.zip +# → build/distributions/claude-code-native-5.8.0.zip ``` Install it with **Settings ▸ Plugins ▸ ⚙ ▸ Install Plugin from Disk**. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2b95fe41..9feca591 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,18 @@ +## v5.8.0 — 2026-08-28 + +**Pick a chat up from your phone.** Remote Control connects a chat running in your IDE to +claude.ai/code or the Claude mobile app, and Claude keeps running on your machine the whole +time — your filesystem, your MCP servers and your project configuration stay the ones in use. +Start a task at the desk, follow it or steer it from the couch, and carry on in the IDE. + +**Two ways in, and neither lies to you.** A phone button sits in the chat's button row, left of +the guard's shield, and *Remote control* is in the ⚙ menu. Both wait for the binary before they +light up, so a control that looks on is on. If the request is refused the button turns red and +its tooltip says why, with the same reason spelled out in the chat. + +**What it needs.** Remote Control has to be enabled for your Claude account, and on Team and +Enterprise plans an organisation Owner has to turn it on first. + ## v5.7.0 — 2026-08-21 **The Sensitive Guard stops being invisible.** It keeps a log of every alert it raises — in the IDE's diff --git a/build.gradle.kts b/build.gradle.kts index 70c33758..595a0329 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ plugins { } group = "dev.lain" -version = "5.7.1" +version = "5.8.0" repositories { mavenCentral() diff --git a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt index 7d456da8..73b40a02 100644 --- a/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt +++ b/src/main/kotlin/dev/lain/claudejb/util/PluginIdentity.kt @@ -2,7 +2,7 @@ package dev.lain.claudejb.util object PluginIdentity { - const val PLUGIN_VERSION = "5.7.1" + const val PLUGIN_VERSION = "5.8.0" private const val PROJECT_URL = "https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains" From 5d229c3b9e51985d1ebd6f63b4b31823f84215ad Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 17:13:47 +0200 Subject: [PATCH 6/8] fix(protocol): drop a permission card the binary has withdrawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approving a call from the phone left the card sitting in the chat forever. With Remote Control on, the binary forwards the prompt to claude.ai as well, and when the remote answers it withdraws the one it left here — over a top-level control_cancel_request frame carrying the request_id it echoes. The parser had no entry for that type, so it fell through to Other and was dropped, and nothing ever took the card down. The withdrawal writes no reply. The binary already has its answer and is no longer waiting, so answering a request that was retracted would be inventing a second verdict. It closes the review diff the same way a rejection does, because a card that leaves without closing it strands the pane. This is the connected surface the feature commit missed: enabling Remote Control adds an inbound frame the plugin never had to understand before. --- src/main/kotlin/dev/lain/claudejb/protocol/ClaudeEvent.kt | 2 ++ .../kotlin/dev/lain/claudejb/protocol/ProtocolParser.kt | 6 ++++++ src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt | 2 ++ src/main/kotlin/dev/lain/claudejb/session/SessionCards.kt | 7 +++++++ src/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt | 2 +- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/lain/claudejb/protocol/ClaudeEvent.kt b/src/main/kotlin/dev/lain/claudejb/protocol/ClaudeEvent.kt index 5a4c7241..d7b644cb 100644 --- a/src/main/kotlin/dev/lain/claudejb/protocol/ClaudeEvent.kt +++ b/src/main/kotlin/dev/lain/claudejb/protocol/ClaudeEvent.kt @@ -78,6 +78,8 @@ sealed interface ClaudeEvent { data class UnsupportedControlRequest(val requestId: String, val subtype: String?) : Control + data class ControlCancel(val requestId: String) : Control + data class ControlResult( val requestId: String, val success: Boolean, diff --git a/src/main/kotlin/dev/lain/claudejb/protocol/ProtocolParser.kt b/src/main/kotlin/dev/lain/claudejb/protocol/ProtocolParser.kt index 04e5cc45..aaee9c87 100644 --- a/src/main/kotlin/dev/lain/claudejb/protocol/ProtocolParser.kt +++ b/src/main/kotlin/dev/lain/claudejb/protocol/ProtocolParser.kt @@ -18,6 +18,7 @@ object ProtocolParser { put("stream_event", ::parseStreamEvent) put("control_request", ::parseControlRequest) put("control_response", ::parseControlResponse) + put("control_cancel_request", ::parseControlCancel) put("rate_limit_event", ::parseRateLimit) put("keep_alive") { emptyList() } typed("result", ResultMessage.serializer(), ClaudeEvent::Result) @@ -252,6 +253,11 @@ object ProtocolParser { }.getOrDefault(emptyList()) } + private fun parseControlCancel(root: JsonObject): List { + val requestId = root.str("request_id") ?: return emptyList() + return listOf(ClaudeEvent.ControlCancel(requestId)) + } + private fun parseControlResponse(root: JsonObject): List { val response = root["response"] as? JsonObject ?: return emptyList() val requestId = response.str("request_id") ?: return emptyList() diff --git a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt index d9345d4e..cde3e251 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/ClaudeSession.kt @@ -1214,6 +1214,8 @@ class ClaudeSession( is ClaudeEvent.UnsupportedControlRequest -> broker.rejectUnsupported(event.requestId, event.subtype) + is ClaudeEvent.ControlCancel -> edt { cards.withdraw(event.requestId) } + is ClaudeEvent.ControlResult -> controlClient.onControlResult(event) } } diff --git a/src/main/kotlin/dev/lain/claudejb/session/SessionCards.kt b/src/main/kotlin/dev/lain/claudejb/session/SessionCards.kt index f749704f..cbe4ee1e 100644 --- a/src/main/kotlin/dev/lain/claudejb/session/SessionCards.kt +++ b/src/main/kotlin/dev/lain/claudejb/session/SessionCards.kt @@ -77,6 +77,13 @@ class SessionCards( session.systemNotice("Rejected ${request.headline}") } + fun withdraw(requestId: String) { + val request = session.cardManager.remove(requestId) ?: return + session.diffs.closeReviewDiff(requestId) + session.systemNotice("${request.headline} was answered from a connected device.") + firePermissions() + } + fun resolveQuestion(requestId: String, answers: Map) { val request = session.cardManager.remove(requestId) ?: return val updated = buildJsonObject { diff --git a/src/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt b/src/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt index 51bb0d0a..003cc09c 100644 --- a/src/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt +++ b/src/test/kotlin/dev/lain/claudejb/drift/ProtocolSurface.kt @@ -49,7 +49,7 @@ data class ProtocolSurface( val KNOWN_EVENT_TYPES: Set = setOf( "system", "assistant", "user", "stream_event", "result", - "control_request", "control_response", "rate_limit_event", "keep_alive", + "control_request", "control_response", "control_cancel_request", "rate_limit_event", "keep_alive", "auth_status", "tool_progress", "tool_use_summary", "prompt_suggestion", ) From 64481cd8a029a8d8ba8fb3bb9ed6666bee1ce3ac Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 17:13:55 +0200 Subject: [PATCH 7/8] chore(protocol): reconcile the drift baseline with the current tools checkDrift had not been run since the tools moved: it recorded SDK 0.3.233 and binary 2.1.233 against a machine running 0.3.250 and 2.1.250. A drift gate whose baseline is stale reports on a protocol nobody is speaking. The detector updates the tools itself before reporting, which is what moved the lockfile here. The vendored SDK is protocol reference and ships to nobody, and package.json already allowed the newer one, so the range is unchanged. The surface came back fully covered once the withdrawal frame was modelled. --- package-lock.json | 70 +++++++++++++++---------------- scripts/drift-baseline.properties | 4 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 623e6a7b..8f26e531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,23 +22,23 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.233.tgz", - "integrity": "sha512-Dy+YqhggwtbezDy3Ap2pb1sK3bOqnI+sLNnsVjB3AUWvR0QlGnjjrjORXY03Y50I+B1eFRNEcYPAZKRYlCkSLQ==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.250.tgz", + "integrity": "sha512-qT/1cBZs0+xPsQfqVOnwIk6pNW8XBkTpQS5RAXKHYb2XYCKqYc0UmOaeiYU2WeI6HEZKORa5iCaAZyKWGluShw==", "dev": true, "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" }, "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.233", - "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.233", - "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.233", - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.233", - "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.233", - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.233", - "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.233", - "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.233" + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.250", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.250", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.250", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.250", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.250", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.250", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.250", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.250" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", @@ -47,9 +47,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.233.tgz", - "integrity": "sha512-4WDiBZgcrmvTDJjS8RNZwoxGgMz/0EpOM+sYa6EtyjwHTd6It1H/+k5zBckCmBajbgS5/ASCJqdwZzi7dwBl0Q==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.250.tgz", + "integrity": "sha512-tcekW4gR2UH0Q3COBaNPQIdud2lKEbs0HfG2yNKC18hXFPpgbuLCdjq0ndS1lcvC1q8ncPW3oQPUutQt3StICQ==", "cpu": [ "arm64" ], @@ -61,9 +61,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.233.tgz", - "integrity": "sha512-RaaEfNrbqSh77H5NdVF9cJQ0xhAUO92aOv71LSKSdAYModMeUvJN0k22Q7gvmx0TlmqJ+aVyCG8J8gVfgSL9mg==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.250.tgz", + "integrity": "sha512-8Yxmmi76oVEIam+oRgxcL2RtqEkKX9Gp4rh500HmMltjX3Tk/ryjCoJEHoaUdU/LU6vWvfQU5W+dB/SJCQQb2A==", "cpu": [ "x64" ], @@ -75,9 +75,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.233.tgz", - "integrity": "sha512-Az9HjQthYQqRjJCacBtDIAHX3TRGK9WlACNb/UOGAK3JndNzZMprM2mK/t6YmP2cRLJsGyorxL7HZmR9R9HYaw==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.250.tgz", + "integrity": "sha512-exABrKfqDv9rp6UCqOTjd4rH1lH6YZt0IuGFsAEHg52N08i5+4/uRtUlQzMf0VpN/dgqDoxt5jpOB9hH863r9w==", "cpu": [ "arm64" ], @@ -92,9 +92,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.233.tgz", - "integrity": "sha512-Z3uZdzt6xgJ3f4NIgO6lzBYSELULKSq6AL4OsNLBzuaEpVW0iYs1kUCaD9rcMlMrf3cV+Dk/GA/lTCGMgbucjQ==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.250.tgz", + "integrity": "sha512-wpe2UmFrC2wyR+DbG6zIiRmqiFvK8nJjdi++nf3jNn0ZSHvW2BqPrh+JnPJOcmkAYR58IdMIj2jagbQAGtN/rA==", "cpu": [ "arm64" ], @@ -109,9 +109,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.233.tgz", - "integrity": "sha512-jpbhV+n9PnxLiyheQ/HjtHIg/E5/jVsk2Vdu132BSoL/3bsObSmMqKgsqoMutzwRZvtpqRs2RPVcjsC8G4A9Zw==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.250.tgz", + "integrity": "sha512-skqoeRDEjobqtIXmt9GNWJUyjcSvKHEf5QVf4PF9v37CtkJWEt7Hs64yFkahFD1mY34FAUyy/Ij0NrbeGKHPBg==", "cpu": [ "x64" ], @@ -126,9 +126,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.233.tgz", - "integrity": "sha512-kYBIAQCu2f1YITcGbpUN2jfrkAzs59TVAragAhE2z+GrkIcxcpZwmaRY6heMBtaSY8SuyrwgqbCW9hJALYFnEg==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.250.tgz", + "integrity": "sha512-ZMjXn2r00SlS17ozg6RfK+OF7kPo3COrCUTPrKnXZk7AYZ+hE0xqunrkt66LwYNFc+KlvDTAZZHSiZVblTT7rA==", "cpu": [ "x64" ], @@ -143,9 +143,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.233.tgz", - "integrity": "sha512-aO2MaNdmQofyPLKszE4s+Ope/sLJPeI/ZlGdCcjYp7qhji2hgZ4bRWWsOrx5eKjz0gFK5CFFltILkFcNcxCsVg==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.250.tgz", + "integrity": "sha512-PC3pzcV8/bT9+PAM9k4fGMU64YY0F9NV6f/G4vbLMqz4qZAWqQp9AXMea+F5KULeJG2ygaAhmhcDyOzW77HhEw==", "cpu": [ "arm64" ], @@ -157,9 +157,9 @@ ] }, "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.3.233", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.233.tgz", - "integrity": "sha512-TcAYyWPXS5mREZGUksuCZsLIRQjbo/Vriur2PqIhAmgZ1oiqBZO27a90sX60EUczD7yV8wpwOVhVLhUxO0kAEg==", + "version": "0.3.250", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.250.tgz", + "integrity": "sha512-PjJRbJwDHccSUWls5gTiuXMgERit1WrrMQzzRqhhBHGzrlQueHVodrpg7HaN5gtirADJzfINcc7azq8j3qcEYw==", "cpu": [ "x64" ], diff --git a/scripts/drift-baseline.properties b/scripts/drift-baseline.properties index 6c8269db..abc53e17 100644 --- a/scripts/drift-baseline.properties +++ b/scripts/drift-baseline.properties @@ -5,5 +5,5 @@ # These are the last *reconciled* versions: the detector updates the live tools to latest (npm update + # claude --update), then reports any protocol surface the plugin doesn't model yet. After reconciling a # report (and `npm update` having bumped the vendored SDK), bump these to match. -sdk=0.3.233 -binary=2.1.233 +sdk=0.3.250 +binary=2.1.250 From b45639a0a64da9e2618d1f66dd897f327b9bd0c4 Mon Sep 17 00:00:00 2001 From: Lain Date: Fri, 28 Aug 2026 17:14:01 +0200 Subject: [PATCH 8/8] docs(readme): list Remote Control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its own section in the user guide and an entry at the top of What's new. Says what the feature needs before it will work, because the account and organisation gates are the refusal a reader is most likely to meet, and says plainly that the guard still decides first on this machine — connecting a phone changes who can answer a permission, never what the chat may do. --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index df28ecc6..ab053f49 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,29 @@ on your disk is ever removed, and nothing you authored is. A chat that needs you while you are looking elsewhere — a permission, a finished turn, an error — raises a notification and badges its tab. Suppressed for the chat already on screen. +### Remote Control — pick a chat up from your phone + +The **phone button** in the chat's button row, left of the guard's shield, connects that chat to +[claude.ai/code](https://claude.ai/code) or the Claude mobile app. The same switch lives in the ⚙ menu +under **Remote control**. Claude keeps running on your machine the whole time: your files, your MCP +servers and your project configuration stay the ones in use, and the browser or the phone is a second +window onto the conversation already open in the IDE. When the connection carries a session URL, the +chat prints it. + +Messages go both ways, and so do permissions — a call you approve from the phone stops asking here, and +its card leaves the chat by itself. The **Sensitive Guard still decides first**, on this machine, before +anything is offered to anyone to approve: connecting a phone does not widen what a chat is allowed to +do, only who can answer it. That is worth being deliberate about, because the person tapping *Approve* +is no longer necessarily sitting in front of the diff. + +**It has to be enabled for your Claude account**, and on Team and Enterprise plans an organisation Owner +has to turn it on first; the feature needs a claude.ai subscription, so an API key will not do. When the +request is refused the button turns red, its tooltip carries the reason, and the same reason is written +into the chat. Neither control switches itself on until the CLI has said yes, so a lit button means a +live connection. + +Turning it off, from either control, disconnects the remote session and leaves the chat running here. + ### Git The **Git** button in the tool window's title bar opens a chat dedicated to the integration, and with it @@ -611,6 +634,12 @@ Architecture, protocol details and the empirically verified facts about the bina ## What's new +**5.8.0** — [Remote Control](#remote-control--pick-a-chat-up-from-your-phone): a chat running in the IDE +can be driven from claude.ai or the Claude mobile app while Claude keeps executing on your machine. A +phone button in the chat's button row and a row in the ⚙ menu, neither of which lights up until the CLI +has accepted; a refusal names its reason on the button and in the chat. Permission cards answered from +the phone close themselves here. + **5.5.0** — a tab and a transcript per agent, with the whole tree one hover away; a single **Workloads** diagram of everything running across every chat; background tasks that keep their output after they end and survive a restart; a [Git integration](#git) whose write actions are asked of Claude rather than