- System tray icon with connection status - Exec approval dialogs with queue and timeout - Settings persistence to JSON (%LOCALAPPDATA%\Clawdbot) - Auto-start on login via Windows Registry - Notification sounds for approvals and connection events - WebView2 Control UI embedding - 23 unit tests passing - Build scripts and Inno Setup installer template - Community build guide documentation Phase 0: Protocol models, WebSocket client, logging Phase 1: Tray icon, exec approval dialogs, Control UI Phase 2: Settings, auto-start, sounds, settings UI
15 KiB
| title | summary | read_when | |||
|---|---|---|---|---|---|
| Building the Windows Companion App | Complete guide to building the Clawdbot Windows companion from scratch |
|
Building the Clawdbot Windows Companion App
This guide documents the complete process of building the Windows companion app for Clawdbot. It's written for developers who want to understand how it was built, contribute improvements, or use it as a reference for building similar apps.
Overview
The Windows companion app is a native .NET 9 WPF application that:
- Runs in the system tray with connection status indicators
- Connects to the Clawdbot Gateway via WebSocket (Protocol v3)
- Shows exec approval dialogs when AI agents request to run commands
- Embeds the Control UI via WebView2
- Persists user settings to disk
- Supports auto-start on Windows login
- Plays notification sounds for important events
Prerequisites
Before building, ensure you have:
- Windows 10 (1903+) or Windows 11
- .NET 9.0 SDK - Download
- Visual Studio 2022 (recommended) or VS Code with C# extension
- WebView2 Runtime - Ships with Windows 11, or download for Windows 10
Verify your setup:
dotnet --version
# Should show 9.0.x
Architecture
The solution follows a clean 3-project structure:
apps/windows/
├── ClawdbotWindows.sln
├── src/
│ ├── Clawdbot.Windows/ # WPF App (UI layer)
│ │ ├── App.xaml(.cs) # Entry point, DI setup
│ │ ├── MainWindow.xaml(.cs) # WebView2 Control UI
│ │ ├── ExecApprovalDialog.xaml(.cs) # Command approval UI
│ │ ├── SettingsWindow.xaml(.cs) # Settings UI
│ │ ├── SystemTrayIcon.cs # Tray icon + context menu
│ │ └── Assets/ # Icons
│ │
│ ├── Clawdbot.Windows.Core/ # Business logic (no UI)
│ │ ├── GatewayChannel.cs # WebSocket client
│ │ ├── ExecApprovalService.cs # Approval event handling
│ │ ├── ExecApprovalModels.cs # Request/response types
│ │ ├── AppSettings.cs # JSON settings persistence
│ │ ├── AutoStartHelper.cs # Windows Registry auto-start
│ │ ├── NotificationSounds.cs # System sound playback
│ │ ├── AppLogger.cs # File logging
│ │ └── WebView2Helper.cs # Runtime detection
│ │
│ └── Clawdbot.Windows.Protocol/ # Gateway protocol models
│ └── GatewayModels.cs # Protocol v3 types
│
└── tests/
└── Clawdbot.Windows.Tests/ # xUnit tests
├── Phase0ValidationTests.cs
├── ExecApprovalTests.cs
└── SettingsTests.cs
Why This Structure?
- Protocol - Isolated models that match the Gateway's TypeScript types exactly
- Core - Testable business logic with no WPF dependencies
- Windows - WPF-specific UI that consumes Core services
This mirrors the macOS app's ClawdbotKit/Clawdbot split.
Build Process
Step 1: Clone and Navigate
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot/apps/windows
Step 2: Restore and Build
# Debug build (faster, includes debug menu)
dotnet build
# Release build (optimized)
dotnet build --configuration Release
Step 3: Run Tests
dotnet test
Expected output: 23 passing, 7 skipped (skipped tests require a running Gateway)
Step 4: Run the App
# From apps/windows directory
dotnet run --project src\Clawdbot.Windows
# Or run the built executable
.\src\Clawdbot.Windows\bin\Release\net9.0-windows\Clawdbot.exe
The app starts minimized to the system tray. Look for the Clawdbot icon in your system tray (you may need to click the "^" arrow to see hidden icons).
Development Phases
This section documents exactly how the app was built, phase by phase.
Phase 0: Foundation (Gateway Connection)
Goal: Establish WebSocket connection to Gateway, validate protocol compatibility.
Files Created
-
GatewayModels.cs - Protocol types matching TypeScript schema
public record ConnectParams( int MinProtocol, int MaxProtocol, Dictionary<string, JsonElement> Client, // ... ); public record HelloOk( int Protocol, string? Error, string? Gateway ); -
GatewayChannel.cs - WebSocket client with:
- Connection state machine (Disconnected → Connecting → Connected)
- Automatic reconnection with exponential backoff
- Request/response correlation via message IDs
- Event subscription (snapshot, exec approval, etc.)
-
AppLogger.cs - File-based logging to
%LOCALAPPDATA%\Clawdbot\logs\ -
Phase0ValidationTests.cs - Integration tests for Gateway connection
Key Protocol Details
The Gateway uses Protocol v3 with JSON-RPC style messaging:
// Request (client → gateway)
{"id": "1", "method": "config.get", "params": {}}
// Response (gateway → client)
{"id": "1", "ok": {"config": {...}, "configHash": "abc123"}}
// Event (gateway → client, no id)
{"method": "snapshot", "params": {...}}
Phase 1: Core Features (Tray + Approvals)
Goal: System tray icon with exec approval dialogs.
Files Created
-
SystemTrayIcon.cs - Using Hardcodet.NotifyIcon.Wpf
- Context menu with status, connect/disconnect, settings, exit
- Double-click opens main window
- Tooltip shows connection status
-
ExecApprovalDialog.xaml - WPF dialog matching macOS design
- Command display in monospace font
- Context details (working dir, host, agent, security level)
- Countdown timer with progress bar
- Three buttons: Allow Once, Always Allow, Don't Allow
-
ExecApprovalService.cs - Queue management
- Handles multiple pending approvals
- Auto-denies on timeout
- Dispatches to UI thread
-
MainWindow.xaml - WebView2 embedding Control UI
Exec Approval Flow
Gateway → exec.approval event
↓
ExecApprovalService (queue)
↓
ExecApprovalDialog (UI)
↓
User clicks button
↓
GatewayChannel.SendExecApprovalResponse()
↓
Gateway
Phase 2: Production Ready
Goal: Settings persistence, auto-start, notification sounds.
Files Created
-
AppSettings.cs - JSON settings
public class AppSettings { public string GatewayUrl { get; set; } = "ws://127.0.0.1:18789/"; public bool StartOnLogin { get; set; } = false; public bool MinimizeToTray { get; set; } = true; public bool PlaySounds { get; set; } = true; // ... }Settings stored at:
%LOCALAPPDATA%\Clawdbot\settings.json -
AutoStartHelper.cs - Windows Registry integration
// Registry key: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run public static void Enable() { var exePath = Process.GetCurrentProcess().MainModule?.FileName; using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, true); key?.SetValue("Clawdbot", $"\"{exePath}\""); } -
NotificationSounds.cs - Windows system sounds
public static void PlayApprovalRequest() => SystemSounds.Exclamation.Play(); public static void PlayConnected() => SystemSounds.Asterisk.Play(); -
SettingsWindow.xaml - Settings UI with:
- Gateway URL textbox
- Checkboxes for all settings
- Save/Cancel/Reset buttons
- Version and path info display
Integration Points
In App.xaml.cs:
// Load settings on startup
_settings = AppSettings.Load();
// Create channel with settings URL
_channel = new GatewayChannel(_settings.GatewayUrl);
// Play sounds on events
_channel.StateChanged += (_, state) => {
if (_settings.PlaySounds)
{
if (state == GatewayState.Connected)
NotificationSounds.PlayConnected();
}
};
Key Implementation Details
WebSocket Connection
The GatewayChannel manages the WebSocket lifecycle:
public async Task ConnectAsync()
{
State = GatewayState.Connecting;
_ws = new ClientWebSocket();
await _ws.ConnectAsync(new Uri(_gatewayUrl), CancellationToken.None);
// Send connect frame
await SendAsync(new ConnectFrame {
Params = new ConnectParams {
MinProtocol = 3,
MaxProtocol = 3,
// ...
}
});
// Wait for hello response
var hello = await ReceiveHelloAsync();
if (hello.Protocol != 3) throw new Exception("Protocol mismatch");
State = GatewayState.Connected;
// Start receive loop
_ = ReceiveLoopAsync();
}
Exec Approval Queue
Multiple approval requests can arrive while a dialog is open:
private readonly Queue<ExecApprovalRequest> _pendingApprovals = new();
private ExecApprovalDialog? _currentDialog;
public void EnqueueApproval(ExecApprovalRequest request)
{
_pendingApprovals.Enqueue(request);
if (_currentDialog == null)
ShowNextApproval();
}
private void ShowNextApproval()
{
if (!_pendingApprovals.TryDequeue(out var request)) return;
_currentDialog = new ExecApprovalDialog(request);
_currentDialog.Closed += (_, _) => {
_currentDialog = null;
ShowNextApproval(); // Process next in queue
};
_currentDialog.Show();
}
Settings Persistence
Settings auto-save and provide defaults for missing properties:
public static AppSettings Load()
{
var path = GetSettingsFilePath();
if (!File.Exists(path)) return new AppSettings();
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json) ?? new();
}
public void Save()
{
var path = GetSettingsFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions {
WriteIndented = true
});
File.WriteAllText(path, json);
}
Testing
Unit Tests (No Gateway Required)
dotnet test --filter "Category!=Integration"
Tests protocol models, settings persistence, approval models.
Integration Tests (Gateway Required)
Start the Gateway first:
# In WSL2 or Linux terminal
clawdbot gateway run --bind loopback --port 18789
Then run all tests:
dotnet test
Manual Testing Checklist
- App starts and shows tray icon
- Tray icon tooltip shows "Disconnected" initially
- Right-click tray shows context menu
- Settings window opens and saves correctly
- Auto-start toggle modifies registry
- Double-click tray opens main window
- With Gateway running, icon shows "Connected"
- Exec approval dialog appears for test commands (Debug menu)
Configuration
Settings File
Location: %LOCALAPPDATA%\Clawdbot\settings.json
{
"gatewayUrl": "ws://127.0.0.1:18789/",
"startOnLogin": false,
"minimizeToTray": true,
"playSounds": true,
"showConnectionNotifications": true,
"reconnectIntervalSeconds": 5
}
Logs
Location: %LOCALAPPDATA%\Clawdbot\logs\clawdbot-YYYY-MM-DD.log
View live:
Get-Content "$env:LOCALAPPDATA\Clawdbot\logs\clawdbot-$(Get-Date -Format 'yyyy-MM-dd').log" -Wait
Next Steps for Contributors
The following features are planned but not yet implemented. Pick one and contribute!
Phase 3: Distribution (Priority: High)
-
MSIX Installer
- Create
Package.appxmanifest - Configure signing certificate
- Add to Windows Store or sideload
- See: MSIX Packaging Guide
- Create
-
MSI Installer (Alternative)
- Use WiX Toolset v4
- Create
Product.wxswith component structure - See: WiX Quick Start
-
Auto-Update Mechanism
- Check GitHub releases API for new versions
- Download and apply updates
- Consider: Squirrel.Windows or custom solution
Phase 4: Feature Parity with macOS
-
Voice Wake Integration
- Windows Speech Recognition API
- "Hey Clawdbot" wake word detection
- Microphone permission handling
-
Talk Mode
- Audio capture and streaming
- Text-to-speech for responses
- Push-to-talk hotkey
-
Canvas Commands
- Screenshot capture (screen.*)
- Clipboard integration
- File drag-and-drop
-
Global Hotkeys
- Register system-wide keyboard shortcuts
- Quick actions without opening app
Phase 5: Polish
-
Notifications
- Windows Toast notifications
- Action buttons in notifications
- Notification history
-
Theming
- Dark/light mode following Windows setting
- Custom accent colors
- High contrast support
-
Accessibility
- Screen reader support
- Keyboard navigation
- High DPI scaling
Code Generator Enhancement
The project could benefit from a C# code generator similar to the Swift one:
scripts/protocol-gen-csharp.ts → GatewayModels.cs
This would auto-generate Protocol models from TypeBox schemas, keeping Windows in sync with Gateway changes.
Troubleshooting
"Cannot connect to Gateway"
- Ensure Gateway is running:
clawdbot gateway run --bind loopback --port 18789 - Check Gateway URL in settings matches
- Verify no firewall blocking port 18789
- Check logs at
%LOCALAPPDATA%\Clawdbot\logs\
"WebView2 not available"
- Windows 11: Should be pre-installed
- Windows 10: Download from Microsoft
- Check:
WebView2Helper.IsWebView2Available()returns true
"App doesn't start"
- Check for existing process:
Get-Process Clawdbot -ErrorAction SilentlyContinue - Kill if stuck:
Stop-Process -Name Clawdbot -Force - Check logs for errors
"Tray icon not visible"
- Click the "^" arrow in system tray to see hidden icons
- Drag Clawdbot icon to always-visible area
- Windows Settings → Personalization → Taskbar → System tray icons
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make changes in
apps/windows/ - Run tests:
dotnet test - Submit a pull request
Code Style
- Follow C# naming conventions (PascalCase for public members)
- Use
async/awaitfor all I/O operations - Add XML doc comments for public APIs
- Keep files under 500 lines when practical
Pull Request Checklist
- Tests pass locally
- New features have tests
- README updated if needed
- No compiler warnings
- Code formatted consistently
Resources
- Clawdbot Documentation
- Gateway Protocol Reference
- WPF Documentation
- WebView2 Documentation
- Hardcodet TaskbarIcon
License
MIT - see LICENSE in the project root.