Compare commits
7 Commits
feat/ui-im
...
fc9029de97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc9029de97 | ||
|
|
1c764dae3f | ||
|
|
cfec3297a4 | ||
|
|
6e1d64b489 | ||
|
|
f599f8d0af | ||
|
|
9b928c6217 | ||
| c9e38aef88 |
@@ -67,7 +67,7 @@ jobs:
|
|||||||
-c Release -r win-x64 --self-contained true \
|
-c Release -r win-x64 --self-contained true \
|
||||||
/p:Version=$VERSION -o out/worker
|
/p:Version=$VERSION -o out/worker
|
||||||
|
|
||||||
- name: Publish ClaudeDo.Installer (win-x64, single-file)
|
- name: Publish ClaudeDo.Installer (win-x64, single-file, framework-dependent)
|
||||||
env:
|
env:
|
||||||
WORK: ${{ steps.ws.outputs.dir }}
|
WORK: ${{ steps.ws.outputs.dir }}
|
||||||
VERSION: ${{ steps.ver.outputs.version }}
|
VERSION: ${{ steps.ver.outputs.version }}
|
||||||
@@ -75,8 +75,11 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export PATH="$DOTNET_ROOT:$PATH"
|
export PATH="$DOTNET_ROOT:$PATH"
|
||||||
cd "$WORK/src"
|
cd "$WORK/src"
|
||||||
|
# Framework-dependent — WPF runtime pack isn't distributed on Linux SDK;
|
||||||
|
# the previous self-contained bundle crashed at startup (apphost AV).
|
||||||
|
# Target machines need .NET 8 Desktop Runtime (x64).
|
||||||
dotnet publish src/ClaudeDo.Installer/ClaudeDo.Installer.csproj \
|
dotnet publish src/ClaudeDo.Installer/ClaudeDo.Installer.csproj \
|
||||||
-c Release -r win-x64 --self-contained true \
|
-c Release -r win-x64 --self-contained false \
|
||||||
/p:Version=$VERSION /p:PublishSingleFile=true \
|
/p:Version=$VERSION /p:PublishSingleFile=true \
|
||||||
-o out/installer
|
-o out/installer
|
||||||
|
|
||||||
|
|||||||
6
global.json
Normal file
6
global.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "8.0.418",
|
||||||
|
"rollForward": "latestFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,7 @@ public partial class App : Application
|
|||||||
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
||||||
sc.AddSingleton<IInstallStep, RegisterServiceStep>();
|
sc.AddSingleton<IInstallStep, RegisterServiceStep>();
|
||||||
sc.AddSingleton<IInstallStep, CreateShortcutsStep>();
|
sc.AddSingleton<IInstallStep, CreateShortcutsStep>();
|
||||||
|
sc.AddSingleton<IInstallStep, WriteUninstallRegistryStep>();
|
||||||
sc.AddSingleton<WriteInstallManifestStep>();
|
sc.AddSingleton<WriteInstallManifestStep>();
|
||||||
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<WriteInstallManifestStep>());
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<WriteInstallManifestStep>());
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<!-- Allow Linux Gitea runners to publish this WPF project for win-x64; no-op on Windows. -->
|
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Debug: asInvoker so Rider/VS can debug without elevation -->
|
<!-- Debug: asInvoker so Rider/VS can debug without elevation -->
|
||||||
@@ -19,10 +21,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(PublishSingleFile)' == 'true'">
|
<PropertyGroup Condition="'$(PublishSingleFile)' == 'true'">
|
||||||
|
<!-- Framework-dependent: the WPF runtime pack isn't distributed for cross-compile
|
||||||
|
on Linux CI, which made self-contained bundles crash on startup with AV in the
|
||||||
|
apphost. Target machines already have the .NET 8 Desktop Runtime. -->
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
|
||||||
<PublishTrimmed>false</PublishTrimmed>
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Installer.Steps;
|
using ClaudeDo.Installer.Steps;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace ClaudeDo.Installer.Core;
|
namespace ClaudeDo.Installer.Core;
|
||||||
|
|
||||||
@@ -36,6 +38,17 @@ public sealed class UninstallRunner
|
|||||||
progress.Report("Unregistering service...");
|
progress.Report("Unregistering service...");
|
||||||
await ProcessRunner.RunAsync("sc.exe", $"delete {StopServiceStep.ServiceName}", null, progress, ct);
|
await ProcessRunner.RunAsync("sc.exe", $"delete {StopServiceStep.ServiceName}", null, progress, ct);
|
||||||
|
|
||||||
|
// 3b) Remove Apps & Features registry entry (best-effort).
|
||||||
|
progress.Report("Removing Add/Remove Programs entry...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Registry.LocalMachine.DeleteSubKeyTree(WriteUninstallRegistryStep.UninstallKeyPath, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
progress.Report($"Warning: could not delete uninstall registry key: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
// 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest).
|
// 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest).
|
||||||
progress.Report("Removing shortcuts...");
|
progress.Report("Removing shortcuts...");
|
||||||
TryDeleteFile(Path.Combine(
|
TryDeleteFile(Path.Combine(
|
||||||
@@ -63,6 +76,21 @@ public sealed class UninstallRunner
|
|||||||
failures.Add($"app data ({appData}): {err}");
|
failures.Add($"app data ({appData}): {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7) If we were launched from inside the install dir (Apps & Features case),
|
||||||
|
// our own exe is still locked — schedule a cmd.exe trampoline to finish
|
||||||
|
// the deletion after this process exits. Best-effort: if this fails the
|
||||||
|
// user is left with an empty <uninstaller> folder which is harmless.
|
||||||
|
var runningExe = Environment.ProcessPath;
|
||||||
|
if (runningExe is not null
|
||||||
|
&& IsInsideDirectory(runningExe, _context.InstallDirectory)
|
||||||
|
&& Directory.Exists(_context.InstallDirectory))
|
||||||
|
{
|
||||||
|
progress.Report("Scheduling final cleanup after exit...");
|
||||||
|
TryScheduleTrampolineDelete(_context.InstallDirectory);
|
||||||
|
// The trampoline will finish the job — clear the residual failure entry for the install dir.
|
||||||
|
failures.RemoveAll(f => f.StartsWith("install dir"));
|
||||||
|
}
|
||||||
|
|
||||||
if (failures.Count > 0)
|
if (failures.Count > 0)
|
||||||
{
|
{
|
||||||
return StepResult.Fail(
|
return StepResult.Fail(
|
||||||
@@ -74,6 +102,37 @@ public sealed class UninstallRunner
|
|||||||
return StepResult.Ok();
|
return StepResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsInsideDirectory(string filePath, string directory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var full = Path.GetFullPath(filePath);
|
||||||
|
var dir = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
return full.StartsWith(dir, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryScheduleTrampolineDelete(string installDir)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pid = Environment.ProcessId;
|
||||||
|
// Wait for this process to exit, then recursively remove the install dir.
|
||||||
|
// /B timeout avoids a visible window; ping as a portable sleep; rmdir /S /Q is silent.
|
||||||
|
var cmd = $"/C start \"\" /MIN cmd /C \"ping 127.0.0.1 -n 3 >nul & rmdir /S /Q \"\"{installDir}\"\"\"";
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "cmd.exe",
|
||||||
|
Arguments = cmd,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Guards against catastrophic recursive-delete paths. The install dir must be
|
/// Guards against catastrophic recursive-delete paths. The install dir must be
|
||||||
/// a non-root directory with a non-empty name (e.g. reject "", "C:\\", "/").
|
/// a non-root directory with a non-empty name (e.g. reject "", "C:\\", "/").
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
|
|||||||
Steps.Add(new StepViewModel("Initialize Database"));
|
Steps.Add(new StepViewModel("Initialize Database"));
|
||||||
Steps.Add(new StepViewModel("Register Windows Service"));
|
Steps.Add(new StepViewModel("Register Windows Service"));
|
||||||
Steps.Add(new StepViewModel("Create Shortcuts"));
|
Steps.Add(new StepViewModel("Create Shortcuts"));
|
||||||
|
Steps.Add(new StepViewModel("Register in Add/Remove Programs"));
|
||||||
Steps.Add(new StepViewModel("Write Install Manifest"));
|
Steps.Add(new StepViewModel("Write Install Manifest"));
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -82,7 +83,21 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
|
|||||||
|
|
||||||
step.Status = p.Status;
|
step.Status = p.Status;
|
||||||
if (p.Message is not null)
|
if (p.Message is not null)
|
||||||
|
{
|
||||||
|
// Messages starting with "\r" overwrite the previous line (live progress).
|
||||||
|
if (p.Message.StartsWith('\r'))
|
||||||
|
{
|
||||||
|
var line = p.Message[1..];
|
||||||
|
if (step.Messages.Count > 0 && step.Messages[^1].StartsWith(" "))
|
||||||
|
step.Messages[^1] = line;
|
||||||
|
else
|
||||||
|
step.Messages.Add(line);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
step.Messages.Add(p.Message);
|
step.Messages.Add(p.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (p.Status is StepStatus.Running && !step.IsExpanded)
|
if (p.Status is StepStatus.Running && !step.IsExpanded)
|
||||||
step.IsExpanded = true;
|
step.IsExpanded = true;
|
||||||
|
|||||||
@@ -44,9 +44,18 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
||||||
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
||||||
|
|
||||||
progress.Report($"Downloading {zipAsset.Name} ({zipAsset.Size / (1024 * 1024)} MB)...");
|
var totalMb = zipAsset.Size / (1024 * 1024);
|
||||||
|
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
||||||
|
long lastReportedMb = -1;
|
||||||
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
||||||
new Progress<long>(b => progress.Report($" {b / (1024 * 1024)} MB downloaded")),
|
new Progress<long>(b =>
|
||||||
|
{
|
||||||
|
var mb = b / (1024 * 1024);
|
||||||
|
if (mb == lastReportedMb) return;
|
||||||
|
lastReportedMb = mb;
|
||||||
|
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
|
||||||
|
progress.Report($"\r {mb} / {totalMb} MB downloaded");
|
||||||
|
}),
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
progress.Report("Downloading checksums...");
|
progress.Report("Downloading checksums...");
|
||||||
|
|||||||
@@ -23,6 +23,24 @@ public sealed class RegisterServiceStep : IInstallStep
|
|||||||
progress.Report("Removing existing service registration (if any)...");
|
progress.Report("Removing existing service registration (if any)...");
|
||||||
await RunSc($"delete {ServiceName}", ctx, progress, ct, ignoreErrors: true);
|
await RunSc($"delete {ServiceName}", ctx, progress, ct, ignoreErrors: true);
|
||||||
|
|
||||||
|
// Wait for the service to actually disappear from SCM. `sc delete` returns
|
||||||
|
// immediately but the service stays "marked for deletion" until every open
|
||||||
|
// handle (services.msc, Task Manager, a prior sc query process) is closed.
|
||||||
|
// Poll up to 30s — then fail with actionable guidance if it's still there.
|
||||||
|
progress.Report("Waiting for prior service registration to clear...");
|
||||||
|
for (var i = 0; i < 30; i++)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var (queryExit, _) = await RunSc($"query {ServiceName}", ctx, progress, ct, ignoreErrors: true);
|
||||||
|
if (queryExit != 0) break; // service no longer registered — good
|
||||||
|
if (i == 29)
|
||||||
|
return StepResult.Fail(
|
||||||
|
$"Service '{ServiceName}' is marked for deletion but hasn't cleared after 30s. " +
|
||||||
|
"Close any open Services console (services.msc), Task Manager Services tab, or " +
|
||||||
|
"Event Viewer showing the service, then retry. A reboot will also clear it.");
|
||||||
|
await Task.Delay(1000, ct);
|
||||||
|
}
|
||||||
|
|
||||||
// Create service
|
// Create service
|
||||||
var startType = ctx.AutoStart ? "auto" : "demand";
|
var startType = ctx.AutoStart ? "auto" : "demand";
|
||||||
var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}";
|
var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}";
|
||||||
@@ -35,6 +53,10 @@ public sealed class RegisterServiceStep : IInstallStep
|
|||||||
|
|
||||||
progress.Report("Creating service...");
|
progress.Report("Creating service...");
|
||||||
var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct);
|
var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct);
|
||||||
|
if (exitCode == 1072)
|
||||||
|
return StepResult.Fail(
|
||||||
|
$"Service '{ServiceName}' is still marked for deletion. " +
|
||||||
|
"Close services.msc / Task Manager / Event Viewer and retry, or reboot.");
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
return StepResult.Fail($"sc.exe create failed (exit {exitCode}): {output}");
|
return StepResult.Fail($"sc.exe create failed (exit {exitCode}): {output}");
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ public sealed class StopServiceStep : IInstallStep
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (stopExit, _) = await ProcessRunner.RunAsync("sc.exe", $"stop {ServiceName}", null, progress, ct);
|
var (stopExit, _) = await ProcessRunner.RunAsync("sc.exe", $"stop {ServiceName}", null, progress, ct);
|
||||||
|
// 1062 = ERROR_SERVICE_NOT_ACTIVE — registered but not running, treat as already stopped.
|
||||||
|
if (stopExit == 1062)
|
||||||
|
{
|
||||||
|
progress.Report("Service was registered but not running.");
|
||||||
|
return StepResult.Ok();
|
||||||
|
}
|
||||||
if (stopExit != 0)
|
if (stopExit != 0)
|
||||||
return StepResult.Fail($"sc.exe stop failed with exit code {stopExit}");
|
return StepResult.Fail($"sc.exe stop failed with exit code {stopExit}");
|
||||||
|
|
||||||
|
|||||||
81
src/ClaudeDo.Installer/Steps/WriteUninstallRegistryStep.cs
Normal file
81
src/ClaudeDo.Installer/Steps/WriteUninstallRegistryStep.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System.IO;
|
||||||
|
using ClaudeDo.Installer.Core;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Installer.Steps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers ClaudeDo under <c>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo</c>
|
||||||
|
/// so it shows up in Windows "Apps & Features" / "Programs and Features".
|
||||||
|
/// Also copies the running installer into the install directory so there is an exe
|
||||||
|
/// for UninstallString to reference after the temp-extracted single-file bundle is gone.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteUninstallRegistryStep : IInstallStep
|
||||||
|
{
|
||||||
|
internal const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo";
|
||||||
|
|
||||||
|
public string Name => "Register in Add/Remove Programs";
|
||||||
|
|
||||||
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var uninstallDir = Path.Combine(ctx.InstallDirectory, "uninstaller");
|
||||||
|
Directory.CreateDirectory(uninstallDir);
|
||||||
|
var targetExe = Path.Combine(uninstallDir, "ClaudeDo.Installer.exe");
|
||||||
|
|
||||||
|
// Copy the running installer so Apps & Features has a stable exe to launch —
|
||||||
|
// the single-file temp extract is gone once this process exits.
|
||||||
|
var sourceExe = Environment.ProcessPath
|
||||||
|
?? throw new InvalidOperationException("Cannot resolve running installer path.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
progress.Report("Copying uninstaller binary...");
|
||||||
|
File.Copy(sourceExe, targetExe, overwrite: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StepResult.Fail($"Failed to copy uninstaller exe: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report("Writing Add/Remove Programs entry...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath, writable: true);
|
||||||
|
if (key is null)
|
||||||
|
return StepResult.Fail("Could not open uninstall registry key (permission denied?).");
|
||||||
|
|
||||||
|
key.SetValue("DisplayName", "ClaudeDo", RegistryValueKind.String);
|
||||||
|
key.SetValue("DisplayVersion", ctx.InstallerVersion ?? "0.0.0", RegistryValueKind.String);
|
||||||
|
key.SetValue("Publisher", "Mika Kuns", RegistryValueKind.String);
|
||||||
|
key.SetValue("InstallLocation", ctx.InstallDirectory, RegistryValueKind.String);
|
||||||
|
key.SetValue("UninstallString", $"\"{targetExe}\"", RegistryValueKind.String);
|
||||||
|
key.SetValue("DisplayIcon", targetExe, RegistryValueKind.String);
|
||||||
|
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||||
|
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||||
|
|
||||||
|
// Best-effort install size (KB) — scan install dir.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sizeKb = (int)(DirectorySizeBytes(ctx.InstallDirectory) / 1024);
|
||||||
|
key.SetValue("EstimatedSize", sizeKb, RegistryValueKind.DWord);
|
||||||
|
}
|
||||||
|
catch { /* best-effort only */ }
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StepResult.Fail($"Failed to write uninstall registry: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
return StepResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long DirectorySizeBytes(string path)
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
try { total += new FileInfo(file).Length; } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,6 +184,34 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBox toggle button (dropdown arrow chrome) -->
|
||||||
|
<ControlTemplate x:Key="ComboBoxToggleButtonTemplate" TargetType="ToggleButton">
|
||||||
|
<Border x:Name="Bd"
|
||||||
|
Background="{StaticResource IslandBgBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderSubtleBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="4">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="20"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Path Grid.Column="1"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Fill="{StaticResource TextSecondaryBrush}"
|
||||||
|
Data="M 0 0 L 4 4 L 8 0 Z"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
|
||||||
<!-- ComboBox -->
|
<!-- ComboBox -->
|
||||||
<Style TargetType="ComboBox">
|
<Style TargetType="ComboBox">
|
||||||
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
@@ -191,6 +219,71 @@
|
|||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBox">
|
||||||
|
<Grid>
|
||||||
|
<ToggleButton x:Name="ToggleButton"
|
||||||
|
Template="{StaticResource ComboBoxToggleButtonTemplate}"
|
||||||
|
Focusable="False"
|
||||||
|
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
ClickMode="Press"/>
|
||||||
|
<ContentPresenter x:Name="ContentSite"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
Content="{TemplateBinding SelectionBoxItem}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||||
|
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
|
TextElement.Foreground="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Popup x:Name="Popup"
|
||||||
|
Placement="Bottom"
|
||||||
|
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||||
|
AllowsTransparency="True" Focusable="False"
|
||||||
|
PopupAnimation="Slide">
|
||||||
|
<Border x:Name="DropDownBorder"
|
||||||
|
Background="{StaticResource IslandBgBrush}"
|
||||||
|
BorderBrush="{StaticResource BorderSubtleBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="4"
|
||||||
|
MinWidth="{TemplateBinding ActualWidth}"
|
||||||
|
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||||
|
<ScrollViewer SnapsToDevicePixels="True">
|
||||||
|
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBoxItem — dark dropdown rows -->
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBoxItem">
|
||||||
|
<Border x:Name="Bd"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- CheckBox -->
|
<!-- CheckBox -->
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
Width="720" Height="520"
|
Width="720" Height="520"
|
||||||
MinWidth="620" MinHeight="460"
|
MinWidth="620" MinHeight="460"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Background="{StaticResource WindowBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
FontFamily="Segoe UI"
|
||||||
|
FontSize="13"
|
||||||
d:DataContext="{d:DesignInstance views:SettingsViewModel}"
|
d:DataContext="{d:DesignInstance views:SettingsViewModel}"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
Width="720" Height="520"
|
Width="720" Height="520"
|
||||||
MinWidth="620" MinHeight="460"
|
MinWidth="620" MinHeight="460"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Background="{StaticResource WindowBgBrush}"
|
||||||
|
Foreground="{StaticResource TextPrimaryBrush}"
|
||||||
|
FontFamily="Segoe UI"
|
||||||
|
FontSize="13"
|
||||||
d:DataContext="{d:DesignInstance views:WizardViewModel}"
|
d:DataContext="{d:DesignInstance views:WizardViewModel}"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ var cfg = WorkerConfig.Load();
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// When launched by the Windows SCM, speak the Service Control Protocol so SCM
|
||||||
|
// doesn't think we crashed (~30s timeout). No-op when running interactively.
|
||||||
|
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
|
||||||
|
|
||||||
// Initialize DB schema before the host starts accepting connections.
|
// Initialize DB schema before the host starts accepting connections.
|
||||||
var dbFactory = new SqliteConnectionFactory(cfg.DbPath);
|
var dbFactory = new SqliteConnectionFactory(cfg.DbPath);
|
||||||
SchemaInitializer.Apply(dbFactory);
|
SchemaInitializer.Apply(dbFactory);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<!-- Allow Linux Gitea runners to build this Windows-targeted project; no-op on Windows. -->
|
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user