Open Launcher Host access from Core

Show a direct Open Host access action when Launcher settings disable the gateway while preserving Reconnect for Core-originated disconnects. Use the bounded Launcher bridge so Core opens the trusted settings editor instead of re-enabling access itself.

Alessandro committed Jul 16, 2026 at 13:37 UTC cfd0df4a62fb186699d62d9d2c56e2e6b777370f
4 files changed +52 -12
plugins/_a0_connector/AGENTS.md
+6 -4
@@ -51,10 +51,12 @@
51 failures through the shared notification system. Use the standard WebUI
52 border-radius tokens for its controls and popover. After Disconnect is
53 acknowledged, keep the popover open and turn that same action into Reconnect;
54 - use only the narrow Launcher WebContents bridge so this also works in a
55 - detached Instance window. `/computer-use on` may use that same bounded bridge
56 - to request approval for the current lease, while `/computer-use off` disables
57 - only the gateway scope. Pausing remains the reversible master switch.
54 + when Host access was turned off in Launcher settings, open that Launcher-owned
55 + editor instead of silently re-granting access. Use only the narrow Launcher
56 + WebContents bridge so both actions also work in a detached Instance window.
57 + `/computer-use on` may use that same bounded bridge to request approval for
58 + the current lease, while `/computer-use off` disables only the gateway scope.
59 + Pausing remains the reversible master switch.
60 - File operation results may arrive as chunked JSON/base64
61 `connector_file_op_result` frames; resolve the pending file operation only
62 after all chunks for the `op_id` are assembled.
plugins/_a0_connector/extensions/webui/sync-status-end/launcher-gateway.html
+8 -6
@@ -103,18 +103,20 @@
103 </template>
104 </div>
105
106 - <p class="launcher-gateway-empty" x-show="!$store.launcherGateway.gateway">
107 - Host access is not connected for this Launcher tab.
108 - </p>
106 + <p
107 + class="launcher-gateway-empty"
108 + x-show="!$store.launcherGateway.gateway"
109 + x-text="$store.launcherGateway.unavailableMessage"
110 + ></p>
111
112 <button
113 type="button"
114 class="launcher-gateway-connection-action"
115 :class="{ 'is-reconnect': !$store.launcherGateway.gateway }"
114 - x-show="$store.launcherGateway.gateway || $store.launcherGateway.reconnectAvailable"
116 + x-show="$store.launcherGateway.gateway || $store.launcherGateway.reconnectAvailable || $store.launcherGateway.canOpenSettings"
117 :disabled="$store.launcherGateway.saving"
116 - @click="$store.launcherGateway.gateway ? $store.launcherGateway.emergencyDisconnect() : $store.launcherGateway.reconnect()"
117 - x-text="$store.launcherGateway.saving ? ($store.launcherGateway.gateway ? 'Disconnecting…' : 'Reconnecting…') : ($store.launcherGateway.gateway ? 'Disconnect' : 'Reconnect')"
118 + @click="$store.launcherGateway.gateway ? $store.launcherGateway.emergencyDisconnect() : ($store.launcherGateway.reconnectAvailable ? $store.launcherGateway.reconnect() : $store.launcherGateway.openSettings())"
119 + x-text="$store.launcherGateway.saving ? ($store.launcherGateway.gateway ? 'Disconnecting…' : ($store.launcherGateway.reconnectAvailable ? 'Reconnecting…' : 'Opening…')) : ($store.launcherGateway.gateway ? 'Disconnect' : ($store.launcherGateway.reconnectAvailable ? 'Reconnect' : 'Open Host access'))"
120 ></button>
121 </section>
122 </div>
plugins/_a0_connector/webui/launcher-gateway-store.js
+32
@@ -21,6 +21,10 @@ const model = {
21 return typeof window.a0LauncherHost?.reconnect === "function";
22 },
23
24 + get canOpenSettings() {
25 + return typeof window.a0LauncherHost?.openSettings === "function";
26 + },
27 +
28 get gateway() {
29 return this.state === "disconnected" ? null : this.status?.gateway || null;
30 },
@@ -59,6 +63,16 @@ const model = {
63 return [...new Set(messages)];
64 },
65
66 + get unavailableMessage() {
67 + if (this.reconnectAvailable) return "Host access was disconnected for this Launcher tab.";
68 + if (this.state === "disconnected") {
69 + return this.canOpenSettings
70 + ? "Host access is off in A0 Launcher."
71 + : "Host access is off. Use the computer icon beside Reload in the Launcher header to turn it on.";
72 + }
73 + return "Open Host access in A0 Launcher for connection details.";
74 + },
75 +
76 onMount() {
77 if (!this.visible || this.intervalId) return;
78 void this.refresh();
@@ -167,6 +181,24 @@ const model = {
181 }
182 },
183
184 + async openSettings() {
185 + if (this.saving) return;
186 + this.saving = true;
187 + try {
188 + const response = await window.a0LauncherHost?.openSettings?.();
189 + if (!response?.ok) throw new Error(response?.message || "Host access settings could not open.");
190 + this.open = false;
191 + } catch (error) {
192 + console.error("Failed to open Launcher host settings:", error);
193 + void toastFrontendError(
194 + error?.message || "Host access settings could not open.",
195 + "Host access",
196 + );
197 + } finally {
198 + this.saving = false;
199 + }
200 + },
201 +
202 async control(payload) {
203 if (this.saving) return;
204 this.saving = true;
tests/test_a0_connector_launcher_gateway.py
+6 -2
@@ -78,8 +78,9 @@ def test_launcher_gateway_indicator_joins_sync_status_without_visual_noise() ->
78 assert "launcher-gateway-trigger-label" not in source
79 assert "launcher-gateway-dot" not in source
80 assert ":title=" not in source
81 - assert "'Disconnect' : 'Reconnect'" in source
82 - assert "$store.launcherGateway.gateway ? $store.launcherGateway.emergencyDisconnect() : $store.launcherGateway.reconnect()" in source
81 + assert "'Disconnect' : ($store.launcherGateway.reconnectAvailable ? 'Reconnect' : 'Open Host access')" in source
82 + assert "$store.launcherGateway.reconnectAvailable ? $store.launcherGateway.reconnect() : $store.launcherGateway.openSettings()" in source
83 + assert "'Reconnect' : 'Open Host access'" in source
84 assert "Emergency disconnect" not in source
85 popover_css = source.split(".launcher-gateway-popover {", 1)[1].split("}", 1)[0]
86 assert "border-radius: var(--border-radius-sm);" in popover_css
@@ -95,7 +96,10 @@ def test_launcher_gateway_disconnect_becomes_reconnect_through_the_launcher_brid
96 ).read_text(encoding="utf-8")
97 assert "window.a0LauncherHost?.getState?.()" in source
98 assert "window.a0LauncherHost?.reconnect?.()" in source
99 + assert "window.a0LauncherHost?.openSettings?.()" in source
100 assert 'typeof window.a0LauncherHost?.reconnect === "function"' in source
101 + assert 'typeof window.a0LauncherHost?.openSettings === "function"' in source
102 + assert "Host access is off in A0 Launcher." in source
103 assert 'state: "disconnected", connected: false, gateway: null' in source
104 assert 'state: "connecting", connected: false, gateway: null' in source
105