Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions installer.iss
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
; OpenClaw Companion Inno Setup Script (WinUI version)
#define MyAppName "OpenClaw Companion"
; Pass /DDevBuild=1 to produce a side-by-side dev installer.
#ifdef DevBuild
#define MyAppName "OpenClaw Companion (Dev)"
#define MyAppId "{{M0LTB0T-TRAY-4PP1-DEV}"
#define MyInstallDir "OpenClawTray-Dev"
#define MyMutex "OpenClawTray-Dev"
#define MyProtocol "openclaw-dev"
#define MyOutputSuffix "-Dev"
#define MyAutoStartName "OpenClawTray-Dev"
#else
#define MyAppName "OpenClaw Companion"
#define MyAppId "{{M0LTB0T-TRAY-4PP1-D3N7}"
#define MyInstallDir "OpenClawTray"
#define MyMutex "OpenClawTray"
#define MyProtocol "openclaw"
#define MyOutputSuffix ""
#define MyAutoStartName "OpenClawTray"
#endif
#define MyAppPublisher "Scott Hanselman"
#define MyAppURL "https://github.com/openclaw/openclaw-windows-node"
#define MyAppExeName "OpenClaw.Tray.WinUI.exe"
Expand All @@ -20,17 +37,17 @@
[Setup]
; Inno requires "{{" to emit a literal opening brace in AppId.
; Do not add a second closing brace here; that creates a malformed uninstall registry key.
AppId={{M0LTB0T-TRAY-4PP1-D3N7}
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL=https://github.com/openclaw/openclaw-windows-node/issues
AppUpdatesURL=https://github.com/openclaw/openclaw-windows-node/releases
DefaultDirName={localappdata}\OpenClawTray
DefaultDirName={localappdata}\{#MyInstallDir}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputBaseFilename=OpenClawCompanion-Setup-{#MyAppArch}
OutputBaseFilename=OpenClawCompanion{#MyOutputSuffix}-Setup-{#MyAppArch}
Compression={#MyCompression}
SolidCompression={#MySolidCompression}
WizardStyle=modern
Expand All @@ -41,7 +58,7 @@ UninstallDisplayIcon={app}\{#MyAppExeName}
; Mutex name matches App.xaml.cs (`new Mutex(true, "OpenClawTray", …)`).
; Tray and Inno run in the same user session, so the bare name resolves
; against Local\OpenClawTray — no Global\ prefix needed.
AppMutex=OpenClawTray
AppMutex={#MyMutex}
#if MyAppArch == "arm64"
ArchitecturesInstallIn64BitMode=arm64
ArchitecturesAllowed=arm64
Expand Down Expand Up @@ -86,17 +103,17 @@ Source: "{#vcRedist}"; DestDir: "{tmp}"; DestName: "vc_redist.exe"; Flags: delet
#endif

[Registry]
Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: ""; ValueData: "URL:OpenClaw Protocol"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
Root: HKCU; Subkey: "Software\Classes\openclaw\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"",0"
Root: HKCU; Subkey: "Software\Classes\openclaw\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}"; ValueType: string; ValueName: ""; ValueData: "URL:OpenClaw Protocol"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"",0"
Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""

[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Gateway Setup"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://setup"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Companion Settings"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://commandcenter"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Chat"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://chat"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\Check for Updates"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://check-updates"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Gateway Setup"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://setup"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Companion Settings"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://commandcenter"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\OpenClaw Chat"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://chat"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\Check for Updates"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://check-updates"; IconFilename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon
Expand Down
22 changes: 15 additions & 7 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public IntPtr GetHubWindowHandle()
private static readonly string DataPath = DataDirOverride
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OpenClawTray");
AppIdentity.IsDev ? "OpenClawTray-Dev" : "OpenClawTray");
private static readonly string DeepLinkPipeName =
DeepLinkSecurityPolicy.BuildCurrentUserScopedPipeName(DataPath);
// Operator/node identity store. In normal installs this is %APPDATA%\OpenClawTray.
Expand All @@ -245,7 +245,7 @@ public IntPtr GetHubWindowHandle()
?? Path.Combine(
Environment.GetEnvironmentVariable("OPENCLAW_TRAY_APPDATA_DIR")
?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenClawTray");
AppIdentity.IsDev ? "OpenClawTray-Dev" : "OpenClawTray");
private readonly AppCrashLogger _crashLogger = new(Path.Combine(DataPath, "crash.log"));
private static readonly AppRunMarker s_runMarker = new(Path.Combine(DataPath, "run.marker"));

Expand Down Expand Up @@ -332,6 +332,14 @@ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExce
e.Handled = true; // Try to prevent crash
}

/// <summary>
/// Returns true if <paramref name="arg"/> looks like a deep link URI for this build variant.
/// Dev builds accept both <c>openclaw-dev://</c> and <c>openclaw://</c> for compatibility.
/// </summary>
private static bool IsDeepLinkArg(string arg) =>
arg.StartsWith($"{AppIdentity.ProtocolScheme}://", StringComparison.OrdinalIgnoreCase)
|| (AppIdentity.IsDev && arg.StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase));

private void OnDomainUnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
_crashLogger.Log("DomainUnhandledException", e.ExceptionObject as Exception);
Expand Down Expand Up @@ -429,7 +437,7 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args)
// (round 2, Scott #5). The suffixed test-isolation variant is
// intentionally not covered by AppMutex — production installs only
// ever use the unsuffixed name.
var mutexName = "OpenClawTray";
var mutexName = AppIdentity.MutexBaseName;
if (DataDirOverride is not null)
{
var hash = System.Security.Cryptography.SHA256.HashData(
Expand All @@ -456,10 +464,10 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args)
{
// Forward deep link args to running instance (command-line or protocol activation)
var deepLink = protocolUri
?? (_startupArgs.Length > 1 && _startupArgs[1].StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase)
?? (_startupArgs.Length > 1 && IsDeepLinkArg(_startupArgs[1])
? _startupArgs[1] : null)
?? (string.Equals(_postSetupLaunch, "chat", StringComparison.OrdinalIgnoreCase)
? "openclaw://chat" : null);
? $"{AppIdentity.ProtocolScheme}://chat" : null);
if (deepLink != null)
{
SendDeepLinkToRunningInstance(deepLink);
Expand Down Expand Up @@ -720,15 +728,15 @@ _dispatcherQueue is null

// Process startup deep link (command-line or MSIX protocol activation)
var startupDeepLink = _pendingProtocolUri
?? (_startupArgs.Length > 1 && _startupArgs[1].StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase)
?? (_startupArgs.Length > 1 && IsDeepLinkArg(_startupArgs[1])
? _startupArgs[1] : null);
if (!setupShownDuringStartup && startupDeepLink != null)
{
await HandleDeepLinkAsync(startupDeepLink);
}
else if (!setupShownDuringStartup && string.Equals(_postSetupLaunch, "chat", StringComparison.OrdinalIgnoreCase))
{
await HandleDeepLinkAsync("openclaw://chat");
await HandleDeepLinkAsync($"{AppIdentity.ProtocolScheme}://chat");
}

Logger.Info("Application started (WinUI 3)");
Expand Down
52 changes: 52 additions & 0 deletions src/OpenClaw.Tray.WinUI/AppIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace OpenClawTray;

/// <summary>
/// Compile-time app identity constants that vary between Dev and Release builds,
/// enabling side-by-side installation of both variants (similar to WinUI Gallery).
/// </summary>
internal static class AppIdentity
{
#if DEV_BUILD
/// <summary>Human-visible app name shown in tray tooltips, window titles, and notifications.</summary>
public const string DisplayName = "OpenClaw Companion (Dev)";

/// <summary>Short name used in tray tooltip prefix.</summary>
public const string TrayName = "OpenClaw Tray (Dev)";

/// <summary>MSIX package identity name (must differ from release for side-by-side).</summary>
public const string PackageIdentityName = "OpenClaw.Companion.Dev";

/// <summary>Windows Registry auto-start value name (must differ so both can auto-start).</summary>
public const string AutoStartRegistryName = "OpenClawTray-Dev";

/// <summary>Single-instance mutex base name.</summary>
public const string MutexBaseName = "OpenClawTray-Dev";

/// <summary>Protocol scheme for deep links.</summary>
public const string ProtocolScheme = "openclaw-dev";

/// <summary>Whether this is a development build.</summary>
public const bool IsDev = true;
#else
/// <summary>Human-visible app name shown in tray tooltips, window titles, and notifications.</summary>
public const string DisplayName = "OpenClaw Companion";

/// <summary>Short name used in tray tooltip prefix.</summary>
public const string TrayName = "OpenClaw Tray";

/// <summary>MSIX package identity name.</summary>
public const string PackageIdentityName = "OpenClaw.Companion";

/// <summary>Windows Registry auto-start value name.</summary>
public const string AutoStartRegistryName = "OpenClawTray";

/// <summary>Single-instance mutex base name.</summary>
public const string MutexBaseName = "OpenClawTray";

/// <summary>Protocol scheme for deep links.</summary>
public const string ProtocolScheme = "openclaw";

/// <summary>Whether this is a development build.</summary>
public const bool IsDev = false;
#endif
}
67 changes: 67 additions & 0 deletions src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>

<!-- Dev build identity: allows side-by-side installation with Release.
Set DevBuild=true explicitly, or it defaults to true for Debug configuration. -->
<PropertyGroup Condition="'$(DevBuild)' == '' And '$(Configuration)' == 'Debug'">
<DevBuild>true</DevBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(DevBuild)' == 'true'">
<DefineConstants>$(DefineConstants);DEV_BUILD</DefineConstants>
</PropertyGroup>

<!-- Map $(Platform) to $(RuntimeIdentifier) so self-contained WinUI F5/debug produces a launchable EXE. -->
<PropertyGroup Condition="'$(RuntimeIdentifier)' == '' and '$(Platform)' == 'x64'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
Expand Down Expand Up @@ -192,6 +201,64 @@
<SyncAppxManifestVersion ManifestPath="$(_AppxManifestPath)" FourPartVersion="$(_AppxManifestVersion)" />
</Target>

<!--
Dev-build MSIX identity patching: changes Identity/Name and DisplayName so the dev
variant installs side-by-side with release (like WinUI Gallery Dev/Release channels).
Runs only for MSIX dev builds; CI's manifest patch step handles Alpha/Release channels.
-->
<UsingTask TaskName="PatchAppxManifestIdentity" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<ManifestPath ParameterType="System.String" Required="true" />
<IdentityName ParameterType="System.String" Required="true" />
<DisplayName ParameterType="System.String" Required="true" />
<ProtocolName ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[
var text = System.IO.File.ReadAllText(ManifestPath);

// Patch Identity Name attribute
var nameRegex = new System.Text.RegularExpressions.Regex(
"(<Identity\\b[^>]*?\\bName\\s*=\\s*[\"'])[^\"']+([\"'])",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
text = nameRegex.Replace(text, "${1}" + IdentityName + "$2", 1);

// Patch Properties/DisplayName element
var propsDisplayRegex = new System.Text.RegularExpressions.Regex(
"(<DisplayName>)[^<]+(</DisplayName>)");
text = propsDisplayRegex.Replace(text, "${1}" + DisplayName + "$2", 2);

// Patch VisualElements DisplayName attribute
var visualRegex = new System.Text.RegularExpressions.Regex(
"(DisplayName\\s*=\\s*\")[^\"]+(\")");
text = visualRegex.Replace(text, "${1}" + DisplayName + "$2");

// Patch Protocol Name attribute (openclaw -> openclaw-dev)
var protocolRegex = new System.Text.RegularExpressions.Regex(
"(<uap:Protocol\\s+Name\\s*=\\s*\")[^\"]+(\")");
text = protocolRegex.Replace(text, "${1}" + ProtocolName + "$2", 1);

System.IO.File.WriteAllText(ManifestPath, text);
Log.LogMessage(MessageImportance.High,
"Patched Package.appxmanifest: Identity=" + IdentityName +
", DisplayName='" + DisplayName + "', Protocol=" + ProtocolName);
]]>
</Code>
</Task>
</UsingTask>

<Target Name="PatchDevAppxManifestIdentity"
AfterTargets="SyncAppxManifestVersionTarget"
BeforeTargets="_GenerateCurrentProjectAppxManifest"
Condition="'$(WindowsPackageType)' == 'MSIX' And '$(DevBuild)' == 'true'">
<PatchAppxManifestIdentity
ManifestPath="$(MSBuildThisFileDirectory)Package.appxmanifest"
IdentityName="OpenClaw.Companion.Dev"
DisplayName="OpenClaw Companion (Dev)"
ProtocolName="openclaw-dev" />
</Target>

<!-- Fail fast on RIDs we have no wxc-exec binary for, so x86/non-Windows
builds don't silently fall back to x64 and ship a broken sandbox.
Suppress with -p:VerifyWxcExecShipped=false for SDK-less unit-test builds. -->
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Tray.WinUI/Services/AutoStartManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace OpenClawTray.Services;
public static class AutoStartManager
{
private const string RegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string AppName = "OpenClawTray";
private static readonly string AppName = AppIdentity.AutoStartRegistryName;

public static bool IsAutoStartEnabled()
{
Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.Tray.WinUI/Services/TrayTooltipBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal string Build()
if (HasRelevantMcpStartupError()) warningCount++;
if (_snapshot.Channels.Length == 0 && isHealthy) warningCount++;

var tooltip = $"OpenClaw Tray - {statusText}; " +
var tooltip = $"{AppIdentity.TrayName} - {statusText}; " +
$"{topology.DisplayName}; " +
$"Channels {channelReady}/{_snapshot.Channels.Length}; " +
$"Nodes {nodeOnline}/{nodeTotal}; " +
Expand All @@ -49,7 +49,7 @@ internal string Build()

if (_snapshot.CurrentActivity != null && !string.IsNullOrEmpty(_snapshot.CurrentActivity.DisplayText))
{
tooltip = $"OpenClaw Tray - {_snapshot.CurrentActivity.DisplayText}; {statusText}";
tooltip = $"{AppIdentity.TrayName} - {_snapshot.CurrentActivity.DisplayText}; {statusText}";
}

return TrayTooltipFormatter.FitShellTooltip(tooltip);
Expand Down
2 changes: 1 addition & 1 deletion tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ public void Setup_IsHostedInTrayAndUsesSelfRestartAfterCompletion()
Assert.Contains("\"--post-setup-restart\"", source);
Assert.Contains("\"--wait-for-pid\"", source);
Assert.Contains("\"--post-setup-launch\"", source);
Assert.Contains("? \"openclaw://chat\" : null", source);
Assert.Contains("$\"{AppIdentity.ProtocolScheme}://chat\"", source);
Assert.Contains("WaitForRestartSourceIfRequested(Environment.GetCommandLineArgs())", source);
AssertInOrder(source, "WaitForRestartSourceIfRequested(Environment.GetCommandLineArgs())", "_mutex = new Mutex");
Assert.DoesNotContain("ResolveSetupEngineUiPath", source);
Expand Down
Loading
Loading