feat: add browser setup guidance entrypoint

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Scott Hanselman 2026-04-27 05:44:45 -07:00
parent cd67ae8485
commit 8bba19ca9d
10 changed files with 161 additions and 17 deletions

View File

@ -310,6 +310,7 @@ OpenClaw registers the `openclaw://` URL scheme for automation and integration:
| `openclaw://config` | Open the config folder |
| `openclaw://diagnostics` | Open the diagnostics JSONL folder |
| `openclaw://support-context` | Copy redacted support context |
| `openclaw://browser-setup` | Copy browser.proxy/browser-control setup guidance |
| `openclaw://restart-ssh-tunnel` | Restart the tray-managed SSH tunnel when enabled |
| `openclaw://send?message=Hello` | Open Quick Send with pre-filled text |
| `openclaw://agent?message=Hello` | Send message directly to the connected gateway |
@ -336,6 +337,7 @@ PowerToys Command Palette extension for quick OpenClaw access.
- **⚙️ Settings** - Open the OpenClaw Tray Settings dialog
- **📄 Open Log File / 📁 Logs / 🗂️ Config / 🧪 Diagnostics** - Open support files and folders
- **📋 Copy Support Context** - Copy redacted Command Center metadata
- **🌐 Copy Browser Setup** - Copy browser.proxy and node-host setup guidance
- **🔁 Restart SSH Tunnel** - Restart the tray-managed SSH tunnel when enabled
### Installation

View File

@ -52,6 +52,7 @@ Open Command Palette (`Win+Alt+Space`), type **"OpenClaw"** — you should see t
| **🗂️ Open Config Folder** | Opens the OpenClaw Tray configuration folder |
| **🧪 Open Diagnostics Folder** | Opens the diagnostics JSONL folder |
| **📋 Copy Support Context** | Copies redacted Command Center support metadata |
| **🌐 Copy Browser Setup** | Copies browser.proxy and node-host setup guidance |
| **🔁 Restart SSH Tunnel** | Restarts the tray-managed SSH tunnel when enabled |
## Usage
@ -119,4 +120,5 @@ Get-AppxPackage -Name '*OpenClaw*' | Remove-AppxPackage
| Open Config Folder | `openclaw://config` |
| Open Diagnostics Folder | `openclaw://diagnostics` |
| Copy Support Context | `openclaw://support-context` |
| Copy Browser Setup | `openclaw://browser-setup` |
| Restart SSH Tunnel | `openclaw://restart-ssh-tunnel` |

View File

@ -90,6 +90,7 @@ OpenClaw Tray responds to `openclaw://` deep links, which can be invoked from a
| `openclaw://config` | Open the config folder |
| `openclaw://diagnostics` | Open the diagnostics JSONL folder |
| `openclaw://support-context` | Copy redacted support context |
| `openclaw://browser-setup` | Copy browser.proxy/browser-control setup guidance |
| `openclaw://restart-ssh-tunnel` | Restart the tray-managed SSH tunnel when enabled |
| `openclaw://agent?message=Hello` | Send a message directly to the connected gateway |

View File

@ -109,6 +109,11 @@ internal sealed partial class OpenClawPage : ListPage
Title = "📋 Copy Support Context",
Subtitle = "Copy redacted Command Center support metadata"
},
new ListItem(new OpenUrlCommand("openclaw://browser-setup"))
{
Title = "🌐 Copy Browser Setup",
Subtitle = "Copy browser.proxy and node-host setup guidance"
},
new ListItem(new OpenUrlCommand("openclaw://restart-ssh-tunnel"))
{
Title = "🔁 Restart SSH Tunnel",

View File

@ -2257,24 +2257,13 @@ public partial class App : Application
Category = "browser",
Title = "Browser proxy host not detected",
Detail = "browser.proxy needs a compatible browser-control host listening on the gateway port + 2.",
RepairAction = "Copy browser-control host guidance",
CopyText = BuildBrowserProxyHostGuidance(port.Port)
RepairAction = "Copy browser setup guidance",
CopyText = StatusDetailWindow.BuildBrowserSetupGuidance(port.Port, topology, tunnel)
};
}
}
}
private static string BuildBrowserProxyHostGuidance(int browserProxyPort)
{
var portText = browserProxyPort is >= 1 and <= 65535 ? browserProxyPort.ToString() : "<gateway-port+2>";
return string.Join(Environment.NewLine, [
$"Start a compatible OpenClaw browser-control host on 127.0.0.1:{portText}.",
"It must run on the same machine as the gateway, or be forwarded to Windows with SSH tunnel mode.",
"Use auth compatible with the Windows node: the saved Settings gateway token, browser-control token, or browser-control password.",
"After it is listening, retry browser.proxy from the gateway."
]);
}
private static string BuildBrowserProxySshForwardHint(int browserProxyPort, TunnelCommandCenterInfo? tunnel)
{
if (browserProxyPort is < 1 or > 65535)
@ -2665,6 +2654,21 @@ public partial class App : Application
}
}
private void CopyBrowserSetupGuidance()
{
try
{
var package = new DataPackage();
package.SetText(StatusDetailWindow.BuildBrowserSetupGuidance(BuildCommandCenterState()));
Clipboard.SetContent(package);
Logger.Info("Copied browser setup guidance from deep link");
}
catch (Exception ex)
{
Logger.Warn($"Failed to copy browser setup guidance from deep link: {ex.Message}");
}
}
private void OnGlobalHotkeyPressed(object? sender, EventArgs e)
{
// Hotkey events are raised from a dedicated Win32 message-loop thread.
@ -2872,6 +2876,7 @@ public partial class App : Application
OpenConfigFolder = OpenConfigFolder,
OpenDiagnosticsFolder = OpenDiagnosticsFolder,
CopySupportContext = CopySupportContext,
CopyBrowserSetupGuidance = CopyBrowserSetupGuidance,
RestartSshTunnel = RestartSshTunnel,
OpenChat = ShowWebChat,
OpenCommandCenter = ShowStatusDetail,

View File

@ -107,6 +107,12 @@ public static class DeepLinkHandler
actions.CopySupportContext?.Invoke();
break;
case "browser-setup":
case "browser-guidance":
case "browser-proxy-setup":
actions.CopyBrowserSetupGuidance?.Invoke();
break;
case "ssh-restart":
case "restart-ssh":
case "restart-ssh-tunnel":
@ -188,6 +194,7 @@ public class DeepLinkActions
public Action? OpenConfigFolder { get; set; }
public Action? OpenDiagnosticsFolder { get; set; }
public Action? CopySupportContext { get; set; }
public Action? CopyBrowserSetupGuidance { get; set; }
public Action? RestartSshTunnel { get; set; }
public Action? OpenChat { get; set; }
public Action? OpenCommandCenter { get; set; }

View File

@ -94,6 +94,7 @@
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock x:Name="OverviewChannelsText" Grid.Row="0" Grid.Column="0" Text="Channels: 0"/>
<TextBlock x:Name="OverviewSessionsText" Grid.Row="0" Grid.Column="1" Text="Sessions: 0"/>
@ -113,6 +114,7 @@
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
<Button Content="Open Logs"
@ -138,8 +140,13 @@
AutomationProperties.AutomationId="CommandCenterRestartSshTunnelButton"
Visibility="Collapsed"/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<Button Content="Copy Browser Setup"
Click="OnCopyBrowserSetup"
AutomationProperties.AutomationId="CommandCenterCopyBrowserSetupButton"/>
</StackPanel>
<TextBlock x:Name="UpdateStatusText"
Grid.Row="2"
Grid.Row="3"
AutomationProperties.AutomationId="CommandCenterUpdateStatusText"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"

View File

@ -377,6 +377,11 @@ public sealed partial class StatusDetailWindow : WindowEx
CopyText(BuildPortDiagnosticsSummary(_state.PortDiagnostics), "[CommandCenter] Copied port diagnostics");
}
private void OnCopyBrowserSetup(object sender, RoutedEventArgs e)
{
CopyText(BuildBrowserSetupGuidance(_state), "[CommandCenter] Copied browser setup guidance");
}
private void OnOpenLogsFolder(object sender, RoutedEventArgs e)
{
OpenFolder(Path.GetDirectoryName(Logger.LogFilePath), "logs");
@ -514,6 +519,104 @@ public sealed partial class StatusDetailWindow : WindowEx
return builder.ToString();
}
internal static string BuildBrowserSetupGuidance(GatewayCommandCenterState state)
{
var browserProxyPort = state.PortDiagnostics
.FirstOrDefault(p => p.Purpose.Equals("Browser proxy host", StringComparison.OrdinalIgnoreCase))
?.Port ?? 0;
return BuildBrowserSetupGuidance(browserProxyPort, state.Topology, state.Tunnel);
}
internal static string BuildBrowserSetupGuidance(
int browserProxyPort,
GatewayTopologyInfo? topology,
TunnelCommandCenterInfo? tunnel)
{
var portText = browserProxyPort is >= 1 and <= 65535
? browserProxyPort.ToString(CultureInfo.InvariantCulture)
: "<gateway-port+2>";
var gatewayHost = string.IsNullOrWhiteSpace(topology?.Host) ? "<gateway-host>" : topology.Host;
var gatewayPort = ResolveGatewayPort(topology?.GatewayUrl);
var gatewayPortText = gatewayPort is >= 1 and <= 65535
? gatewayPort.Value.ToString(CultureInfo.InvariantCulture)
: "<gateway-port>";
var lines = new List<string>
{
"OpenClaw browser proxy setup",
$"Expected local browser-control endpoint: http://127.0.0.1:{portText}/",
"",
"If the Gateway and browser are on this Windows machine:",
"1. Ensure the upstream browser plugin is enabled in the Gateway config.",
"2. Verify the browser control plane:",
" openclaw browser --browser-profile openclaw doctor",
" openclaw browser --browser-profile openclaw start",
" openclaw browser --browser-profile openclaw tabs",
"",
"If the browser is on this Windows machine but the Gateway is remote:",
"1. Run a browser-capable OpenClaw node host on this machine:",
$" openclaw node run --host {gatewayHost} --port {gatewayPortText}",
"2. Or install it as a user service:",
$" openclaw node install --host {gatewayHost} --port {gatewayPortText}",
" openclaw node start",
"3. Keep nodeHost.browserProxy.enabled=true, and configure nodeHost.browserProxy.allowProfiles only if you want to restrict profile access.",
"",
"Gateway policy and auth checks:",
"- The Gateway allowlist must permit browser.proxy for this node.",
"- Browser-control auth must match the saved Gateway token/password in Settings.",
"- Do not paste QR bootstrap tokens into the normal Gateway Token field."
};
if (topology?.UsesSshTunnel == true)
{
lines.Add("");
lines.Add("SSH tunnel mode:");
lines.Add("- Prefer the tray-managed SSH tunnel with Browser proxy bridge enabled; it forwards local-port+2 to remote-port+2 automatically.");
lines.Add($"- Manual forward shape: {BuildBrowserProxySshForwardHint(browserProxyPort, tunnel)}");
}
return string.Join(Environment.NewLine, lines);
}
private static string BuildBrowserProxySshForwardHint(int browserProxyPort, TunnelCommandCenterInfo? tunnel)
{
if (browserProxyPort is < 1 or > 65535)
return "ssh -N -L <local-browser-port>:127.0.0.1:<remote-browser-port> <user>@<host>";
var target = string.IsNullOrWhiteSpace(tunnel?.User) || string.IsNullOrWhiteSpace(tunnel.Host)
? "<user>@<host>"
: $"{tunnel.User}@{tunnel.Host}";
var remoteBrowserPort = TryParseEndpointPort(tunnel?.BrowserProxyRemoteEndpoint) ?? browserProxyPort;
return $"ssh -N -L {browserProxyPort}:127.0.0.1:{remoteBrowserPort} {target}";
}
private static int? TryParseEndpointPort(string? endpoint)
{
if (string.IsNullOrWhiteSpace(endpoint))
return null;
if (Uri.TryCreate($"tcp://{endpoint}", UriKind.Absolute, out var uri) &&
uri.Port is >= 1 and <= 65535)
{
return uri.Port;
}
var portDelimiter = endpoint.LastIndexOf(':');
return portDelimiter >= 0 &&
int.TryParse(endpoint[(portDelimiter + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var port) &&
port is >= 1 and <= 65535
? port
: null;
}
private static int? ResolveGatewayPort(string? gatewayUrl)
{
return Uri.TryCreate(gatewayUrl, UriKind.Absolute, out var uri) && uri.Port is >= 1 and <= 65535
? uri.Port
: null;
}
private static string RedactSupportPath(string? path)
{
if (string.IsNullOrWhiteSpace(path))

View File

@ -96,6 +96,7 @@ public class DeepLinkParserTests
[InlineData("openclaw://config", "config")]
[InlineData("openclaw://diagnostics", "diagnostics")]
[InlineData("openclaw://support-context", "support-context")]
[InlineData("openclaw://browser-setup", "browser-setup")]
[InlineData("openclaw://restart-ssh-tunnel", "restart-ssh-tunnel")]
public void ParseDeepLink_TrayUtilityEntrypoints(string uri, string expectedPath)
{
@ -228,6 +229,7 @@ public class DeepLinkParserTests
[InlineData("openclaw://config", nameof(DeepLinkActions.OpenConfigFolder))]
[InlineData("openclaw://diagnostics", nameof(DeepLinkActions.OpenDiagnosticsFolder))]
[InlineData("openclaw://support-context", nameof(DeepLinkActions.CopySupportContext))]
[InlineData("openclaw://browser-setup", nameof(DeepLinkActions.CopyBrowserSetupGuidance))]
[InlineData("openclaw://restart-ssh-tunnel", nameof(DeepLinkActions.RestartSshTunnel))]
public void Handle_InvokesExpectedAction(string uri, string expectedAction)
{
@ -244,6 +246,7 @@ public class DeepLinkParserTests
OpenConfigFolder = () => invoked = nameof(DeepLinkActions.OpenConfigFolder),
OpenDiagnosticsFolder = () => invoked = nameof(DeepLinkActions.OpenDiagnosticsFolder),
CopySupportContext = () => invoked = nameof(DeepLinkActions.CopySupportContext),
CopyBrowserSetupGuidance = () => invoked = nameof(DeepLinkActions.CopyBrowserSetupGuidance),
RestartSshTunnel = () => invoked = nameof(DeepLinkActions.RestartSshTunnel)
};

View File

@ -151,6 +151,8 @@ public class TrayMenuWindowMarkupTests
Assert.Contains("Open Diagnostics Folder", source);
Assert.Contains(@"openclaw://support-context", source);
Assert.Contains("Copy Support Context", source);
Assert.Contains(@"openclaw://browser-setup", source);
Assert.Contains("Copy Browser Setup", source);
Assert.Contains(@"openclaw://restart-ssh-tunnel", source);
Assert.Contains("Restart SSH Tunnel", source);
}
@ -227,6 +229,8 @@ public class TrayMenuWindowMarkupTests
Assert.Contains("OpenDiagnosticsFolder?.Invoke", source);
Assert.Contains(@"case ""support-context"":", source);
Assert.Contains("CopySupportContext?.Invoke", source);
Assert.Contains(@"case ""browser-setup"":", source);
Assert.Contains("CopyBrowserSetupGuidance?.Invoke", source);
Assert.Contains(@"case ""restart-ssh-tunnel"":", source);
Assert.Contains("RestartSshTunnel?.Invoke", source);
}
@ -308,10 +312,11 @@ public class TrayMenuWindowMarkupTests
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterOpenConfigButton""", xaml);
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterOpenDiagnosticsButton""", xaml);
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterCopySupportContextButton""", xaml);
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterCopyBrowserSetupButton""", xaml);
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterRestartSshTunnelButton""", xaml);
Assert.Contains(@"AutomationProperties.AutomationId=""CommandCenterUpdateStatusText""", xaml);
Assert.Matches(
new Regex(@"<Grid\.RowDefinitions>\s*<RowDefinition/>\s*<RowDefinition/>\s*<RowDefinition/>\s*</Grid\.RowDefinitions>\s*<StackPanel Grid\.Row=""0""", RegexOptions.Singleline),
new Regex(@"<Grid\.RowDefinitions>\s*<RowDefinition/>\s*<RowDefinition/>\s*<RowDefinition/>\s*<RowDefinition/>\s*</Grid\.RowDefinitions>\s*<StackPanel Grid\.Row=""0""", RegexOptions.Singleline),
xaml);
}
@ -391,8 +396,12 @@ public class TrayMenuWindowMarkupTests
Assert.Contains("<remote-gateway-port+2>", appSource);
Assert.Contains("BuildBrowserProxyAuthWarnings(nodes)", appSource);
Assert.Contains("Do not paste QR bootstrap tokens into the normal gateway token field.", appSource);
Assert.Contains("BuildBrowserProxyHostGuidance(port.Port)", appSource);
Assert.Contains("Start a compatible OpenClaw browser-control host", appSource);
Assert.Contains("StatusDetailWindow.BuildBrowserSetupGuidance(port.Port, topology, tunnel)", appSource);
Assert.Contains("Copy browser setup guidance", appSource);
Assert.Contains("openclaw node run --host", source);
Assert.Contains("openclaw browser --browser-profile openclaw doctor", source);
Assert.Contains(@"topology.Host", source);
Assert.DoesNotContain("RedactSupportValue(topology.Host)", source);
var portDiagnosticsSourcePath = Path.Combine(
GetRepositoryRoot(),
"src",