Skip to content

Commit 8ac00c5

Browse files
authored
fix(tray): browser control stuck without shared gateway token (#1066)
* fix(tray): explain browser proxy when shared gateway token is missing Setup-code pairing can connect without a SharedGatewayToken, which silently skipped browser capability declaration and left the UI stuck on Enabled, not active yet. Surface Needs gateway shared token, warn in Command Center, and log the concrete skip reason. Refs: openclaw/openclaw#115989 * fix(tray): separate browser proxy no-client from missing-token UI Only show Needs shared token / Command Center guidance when a node client is attached; disconnected gateways should not tell operators to paste a token. * fix(tray): align browser.proxy missing-token UX across live session surfaces Use manager NodeState for remediation, and share topology-aware remote/SSH endpoint guidance across Connection pill tooltips, Command Center, and app.connection diagnostics so token-only copy does not overpromise.
1 parent d7d153c commit 8ac00c5

22 files changed

Lines changed: 976 additions & 101 deletions

docs/WINDOWS_NODE_TESTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ Local MCP clients also see MCP-only `app.*` commands such as `app.navigate`, `ap
144144
- Ensure Windows notifications are enabled for the app
145145
- Check if notification settings in the app are enabled
146146

147+
### Browser control stays enabled but never declares `browser`
148+
- Setup-code / QR pairing can connect with a device token and leave `GatewayRecord.SharedGatewayToken` empty. Browser control will not declare `browser` / `browser.proxy` until a shared gateway token is saved for that gateway.
149+
- Expect Connection capability pills to say **Needs gateway shared token** (not "Enabled, not active yet") only while the node WebSocket session is live and the shared token is missing. Disconnected or attached-but-disconnected states should ask for reconnect, not a token paste. The pill keeps that short label; its tooltip matches Command Center remediation detail.
150+
- Command Center, Connection pill tooltips, and `app.connection.status` / `app.connection.gateways` use the same live-session rule for the shared-token caveat. For a remote (non-loopback) gateway without an explicit `BrowserControlPort` or SSH browser-proxy forward — including SSH tunnels whose effective URL is `127.0.0.1` — that caveat also mentions the endpoint/forward requirement; the shared token alone is not enough for usable remote browser.proxy.
151+
- Enter the gateway shared token in Settings, save, and reconnect node mode. Bootstrap tokens are not the shared gateway token.
152+
147153
### `browser.proxy` reports no browser-control host
148154
- Confirm the Browser proxy bridge toggle is enabled in Settings, then save and reconnect or re-pair if the gateway keeps an older command snapshot.
149155
- The bridge is local-only: it calls `http://127.0.0.1:<gateway-port+2>` from Windows. For a gateway on `ws://127.0.0.1:18789`, the browser-control host must listen on `127.0.0.1:18791`.

src/OpenClaw.Tray.WinUI/App.CapabilityHandlers.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,16 @@ private void WireAppCapabilityHandlers()
256256
};
257257

258258
connection.GatewaysHandler = () =>
259-
Task.FromResult<object?>(ConnectionDiagnosticsProjection.BuildGateways(
259+
{
260+
var nodeSessionLive = BrowserProxyActivation.IsNodeSessionLive(
261+
_connectionManager?.CurrentSnapshot.NodeState
262+
?? OpenClaw.Connection.RoleConnectionState.Idle);
263+
return Task.FromResult<object?>(ConnectionDiagnosticsProjection.BuildGateways(
260264
_gatewayRegistry?.GetAll() ?? [],
261265
_gatewayRegistry?.ActiveGatewayId,
262-
nodeBrowserProxyEnabled: _settings?.NodeBrowserProxyEnabled != false));
266+
nodeBrowserProxyEnabled: _settings?.NodeBrowserProxyEnabled != false,
267+
nodeSessionLive: nodeSessionLive));
268+
};
263269

264270
connection.ApplySetupCodeHandler = async setupCode =>
265271
{

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3848,6 +3848,8 @@ private AppStateSnapshot CaptureSnapshot()
38483848
EffectiveBrowserControlPort = activeGateway?.BrowserControlPort,
38493849
HasActiveGatewayRecord = activeGateway != null,
38503850
ActiveGatewayHasSharedToken = !string.IsNullOrWhiteSpace(activeGateway?.SharedGatewayToken),
3851+
NodeConnectionState = _connectionManager?.CurrentSnapshot.NodeState
3852+
?? OpenClaw.Connection.RoleConnectionState.Idle,
38513853
ActiveGatewaySshTunnel = activeGateway?.SshTunnel
38523854
};
38533855
}

src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -930,18 +930,37 @@ or NodeCardState.OnNodeRateLimited
930930

931931
if (showSurfaces && settings is not null)
932932
{
933+
var activeGateway = _gatewayRegistry?.GetActive();
934+
var hasSharedGatewayToken = !string.IsNullOrWhiteSpace(
935+
activeGateway?.SharedGatewayToken);
936+
// Same manager NodeState signal as app.connection.* / Command Center.
937+
var nodeSessionLive = BrowserProxyActivation.IsNodeSessionLive(
938+
_connectionManager?.CurrentSnapshot.NodeState
939+
?? OpenClaw.Connection.RoleConnectionState.Idle);
940+
// Match Command Center CaptureSnapshot: active record URL, else settings.
941+
var requiresRemoteBrowserEndpoint =
942+
BrowserProxyActivation.RequiresRemoteBrowserEndpoint(
943+
gatewayUrl: activeGateway?.Url ?? settings.GatewayUrl,
944+
browserControlPort: activeGateway?.BrowserControlPort,
945+
sshTunnel: activeGateway?.SshTunnel);
933946
var pillFp = BuildCapabilityPillFingerprint(
934947
plan.NodeCard,
935948
plan.NodeEffectiveCapabilities,
936949
plan.NodePendingDeclaredCapabilities,
937-
settings);
950+
settings,
951+
hasSharedGatewayToken,
952+
nodeSessionLive,
953+
requiresRemoteBrowserEndpoint);
938954
if (_capabilityPillsFingerprint != pillFp)
939955
{
940956
_capabilityPillsFingerprint = pillFp;
941957
NodeCapabilityPillsHost.Child = BuildCapabilityPills(
942958
plan.NodeEffectiveCapabilities,
943959
plan.NodePendingDeclaredCapabilities,
944-
settings);
960+
settings,
961+
hasSharedGatewayToken,
962+
nodeSessionLive,
963+
requiresRemoteBrowserEndpoint);
945964
}
946965

947966
NodeCapabilityPillsHost.Visibility =
@@ -1194,12 +1213,15 @@ private static Border BuildBulletRow(string text)
11941213
return new Border { Child = grid };
11951214
}
11961215

1197-
private enum CapabilityPillState { Active, Pending, Off }
1216+
private enum CapabilityPillState { Active, Pending, NeedsSharedToken, Off }
11981217

11991218
private WrapPanel BuildCapabilityPills(
12001219
IReadOnlyList<string> effective,
12011220
IReadOnlyList<string> pendingDeclared,
1202-
SettingsManager settings)
1221+
SettingsManager settings,
1222+
bool hasSharedGatewayToken,
1223+
bool nodeSessionLive,
1224+
bool requiresRemoteBrowserEndpoint)
12031225
{
12041226
var panel = new WrapPanel { HorizontalSpacing = 6, VerticalSpacing = 6 };
12051227
var effectiveSet = new HashSet<string>(
@@ -1224,12 +1246,36 @@ private WrapPanel BuildCapabilityPills(
12241246
var shown = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
12251247
foreach (var (name, labelKey, glyph, enabled) in canonical)
12261248
{
1227-
var state = effectiveSet.Contains(name)
1228-
? CapabilityPillState.Active
1229-
: (pendingSet.Contains(name) || enabled)
1230-
? CapabilityPillState.Pending
1231-
: CapabilityPillState.Off;
1232-
panel.Children.Add(MakeCapabilityPill(LocalizationHelper.GetString(labelKey), glyph, state, isHighContrast));
1249+
var kind = name.Equals("browser", StringComparison.OrdinalIgnoreCase)
1250+
? BrowserProxyActivation.ResolveCapabilityPillKind(
1251+
toggleEnabled: enabled,
1252+
effective: effectiveSet.Contains(name),
1253+
pendingDeclared: pendingSet.Contains(name),
1254+
hasSharedGatewayToken: hasSharedGatewayToken,
1255+
nodeSessionLive: nodeSessionLive)
1256+
: effectiveSet.Contains(name)
1257+
? BrowserProxyActivation.CapabilityPillKind.Active
1258+
: (pendingSet.Contains(name) || enabled)
1259+
? BrowserProxyActivation.CapabilityPillKind.PendingApproval
1260+
: BrowserProxyActivation.CapabilityPillKind.Off;
1261+
var state = kind switch
1262+
{
1263+
BrowserProxyActivation.CapabilityPillKind.Active => CapabilityPillState.Active,
1264+
BrowserProxyActivation.CapabilityPillKind.NeedsSharedToken => CapabilityPillState.NeedsSharedToken,
1265+
BrowserProxyActivation.CapabilityPillKind.PendingApproval => CapabilityPillState.Pending,
1266+
_ => CapabilityPillState.Off,
1267+
};
1268+
var remoteForPill = name.Equals("browser", StringComparison.OrdinalIgnoreCase) &&
1269+
kind == BrowserProxyActivation.CapabilityPillKind.NeedsSharedToken
1270+
? requiresRemoteBrowserEndpoint
1271+
: false;
1272+
panel.Children.Add(MakeCapabilityPill(
1273+
LocalizationHelper.GetString(labelKey),
1274+
glyph,
1275+
state,
1276+
isHighContrast,
1277+
kind,
1278+
remoteForPill));
12331279
shown.Add(name);
12341280
}
12351281

@@ -1245,15 +1291,29 @@ private WrapPanel BuildCapabilityPills(
12451291
"system" => (LocalizationHelper.GetString("ConnectionPage_NodeCap_System"), FluentIconCatalog.System),
12461292
_ => (HumanizeNodeToken(name), FluentIconCatalog.System),
12471293
};
1248-
panel.Children.Add(MakeCapabilityPill(label, glyph, state, isHighContrast));
1294+
panel.Children.Add(MakeCapabilityPill(
1295+
label,
1296+
glyph,
1297+
state,
1298+
isHighContrast,
1299+
kind: state == CapabilityPillState.Active
1300+
? BrowserProxyActivation.CapabilityPillKind.Active
1301+
: BrowserProxyActivation.CapabilityPillKind.PendingApproval,
1302+
requiresRemoteBrowserEndpoint: false));
12491303
}
12501304

12511305
return panel;
12521306
}
12531307

12541308
private const double CapabilityPillFillOpacity = 0.14;
12551309

1256-
private Border MakeCapabilityPill(string label, string glyph, CapabilityPillState state, bool isHighContrast)
1310+
private Border MakeCapabilityPill(
1311+
string label,
1312+
string glyph,
1313+
CapabilityPillState state,
1314+
bool isHighContrast,
1315+
BrowserProxyActivation.CapabilityPillKind kind,
1316+
bool requiresRemoteBrowserEndpoint)
12571317
{
12581318
var (fillBrush, iconBrush, textBrush, stateKey, stateGlyph) = state switch
12591319
{
@@ -1277,6 +1337,16 @@ private Border MakeCapabilityPill(string label, string glyph, CapabilityPillStat
12771337
ResolveBrush("SystemFillColorCautionBrush"),
12781338
"ConnectionPage_NodePillState_Pending",
12791339
FluentIconCatalog.StatusWarn),
1340+
CapabilityPillState.NeedsSharedToken => (
1341+
TintBrush(
1342+
"SystemFillColorCriticalBrush",
1343+
"SystemFillColorCriticalBackgroundBrush",
1344+
CapabilityPillFillOpacity,
1345+
isHighContrast),
1346+
ResolveBrush("SystemFillColorCriticalBrush"),
1347+
ResolveBrush("SystemFillColorCriticalBrush"),
1348+
"ConnectionPage_NodePillState_NeedsGatewayToken",
1349+
FluentIconCatalog.StatusWarn),
12801350
_ => (
12811351
ResolveBrush("SubtleFillColorTertiaryBrush"),
12821352
ResolveBrush("TextFillColorTertiaryBrush"),
@@ -1303,14 +1373,18 @@ private Border MakeCapabilityPill(string label, string glyph, CapabilityPillStat
13031373
content.Children.Add(capabilityIcon);
13041374

13051375
var stateText = LocalizationHelper.GetString(stateKey);
1376+
var detailText = BrowserProxyActivation.ResolveCapabilityPillTooltip(
1377+
kind,
1378+
stateText,
1379+
requiresRemoteBrowserEndpoint);
13061380
var labelText = new TextBlock
13071381
{
13081382
Text = label,
13091383
FontSize = 12,
13101384
Foreground = textBrush,
13111385
VerticalAlignment = VerticalAlignment.Center,
13121386
};
1313-
AutomationProperties.SetName(labelText, $"{label}: {stateText}");
1387+
AutomationProperties.SetName(labelText, $"{label}: {detailText}");
13141388
content.Children.Add(labelText);
13151389

13161390
if (stateGlyph != null)
@@ -1334,7 +1408,7 @@ private Border MakeCapabilityPill(string label, string glyph, CapabilityPillStat
13341408
Background = fillBrush,
13351409
Child = content,
13361410
};
1337-
ToolTipService.SetToolTip(pill, stateText);
1411+
ToolTipService.SetToolTip(pill, detailText);
13381412
return pill;
13391413
}
13401414

@@ -1366,7 +1440,10 @@ private static string BuildCapabilityPillFingerprint(
13661440
NodeCardState state,
13671441
IReadOnlyList<string> effective,
13681442
IReadOnlyList<string> pendingDeclared,
1369-
SettingsManager settings)
1443+
SettingsManager settings,
1444+
bool hasSharedGatewayToken,
1445+
bool nodeSessionLive,
1446+
bool requiresRemoteBrowserEndpoint)
13701447
{
13711448
var eff = string.Join(
13721449
",",
@@ -1384,7 +1461,7 @@ private static string BuildCapabilityPillFingerprint(
13841461
settings.NodeLocationEnabled ? '1' : '0',
13851462
settings.NodeTtsEnabled ? '1' : '0',
13861463
settings.NodeSttEnabled ? '1' : '0');
1387-
return $"{state}|{eff}|{pend}|{toggles}";
1464+
return $"{state}|{eff}|{pend}|{toggles}|{(hasSharedGatewayToken ? '1' : '0')}|{(nodeSessionLive ? '1' : '0')}|{(requiresRemoteBrowserEndpoint ? '1' : '0')}";
13881465
}
13891466

13901467
/// <summary>

src/OpenClaw.Tray.WinUI/Services/AppStateSnapshot.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ internal sealed record AppStateSnapshot
3939
public bool HasActiveGatewayRecord { get; init; }
4040
public bool ActiveGatewayHasSharedToken { get; init; }
4141

42+
/// <summary>
43+
/// Manager-owned node role state. Shared-token browser remediation uses
44+
/// <see cref="BrowserProxyActivation.IsNodeSessionLive"/> on this value so
45+
/// Command Center matches <c>app.connection.*</c> diagnostics.
46+
/// </summary>
47+
public OpenClaw.Connection.RoleConnectionState NodeConnectionState { get; init; }
48+
4249
/// <summary>SSH tunnel config from the active GatewayRecord. Null means this gateway is
4350
/// direct (no tunnel), NOT "unknown". Only meaningful when HasActiveGatewayRecord is true.</summary>
4451
public SshTunnelConfig? ActiveGatewaySshTunnel { get; init; }

0 commit comments

Comments
 (0)