diff --git a/CLAUDE.md b/CLAUDE.md
index 48ad4b8..2311621 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -153,7 +153,7 @@ Umstellung des Bestands ist ein eigenes Vorhaben mit niedriger Priorität.
| Dokument | Wofür |
|---|---|
-| [docs/Oberflaechen-Leitfaden.md](docs/Oberflaechen-Leitfaden.md) | **Eingefroren seit 2026-10-02:** Die Avalonia-Oberfläche wird nicht mehr gepflegt und zur Hülle um die WebUI umgebaut (#764). Neue Funktionen nur in API + WebUI |
+| [docs/archiv/Oberflaechen-Leitfaden.md](docs/archiv/Oberflaechen-Leitfaden.md) | **Archiv:** Die Avalonia-Fenster sind mit #764 (Stufe 3) entfernt. `ClawdDotNet` ist Instanzprozess, MCP-Brücke und Starter; einzige Oberfläche ist die WebUI. Neue Funktionen nur in API + WebUI |
| [docs/umsetzungsplaene/WebUI-Only-und-Englisch.md](docs/umsetzungsplaene/WebUI-Only-und-Englisch.md) | Der Umbau zur reinen WebUI und die Englisch-Komplettierung (#764) |
| [docs/WebUI-Leitfaden.md](docs/WebUI-Leitfaden.md) | Arbeiten an der Weboberfläche (`ClawdDotNet.Web`, #494) — Schichtschnitt, API-Fläche, Sicherheitsvorgaben |
| [docs/ToolDevelopmentGuide.md](docs/ToolDevelopmentGuide.md) | Ein neues Agenten-Tool bauen |
diff --git a/deploy/publish.py b/deploy/publish.py
index 944b8ba..da490d4 100644
--- a/deploy/publish.py
+++ b/deploy/publish.py
@@ -90,6 +90,15 @@ def publish(rid: str, out_dir: Path) -> None:
])
merge_manager(manager_dir, out_dir)
+ # #795: Die systemd-Vorlagen gehoeren ins Linux-Paket - sonst steht die Anleitung in
+ # einem Repository, das auf der Zielmaschine niemand hat.
+ if rid.startswith("linux"):
+ units = out_dir / "systemd"
+ units.mkdir(exist_ok=True)
+ for unit in sorted((REPO / "deploy" / "systemd").glob("*.service")):
+ shutil.copy2(unit, units / unit.name)
+ print(f" systemd-Vorlagen: {len(list(units.glob('*.service')))} nach systemd/")
+
def merge_manager(src: Path, dst: Path) -> None:
"""Legt den Manager neben die Instanz.
diff --git a/docs/Roadmap.md b/docs/Roadmap.md
index c7558f3..9bf2ae2 100644
--- a/docs/Roadmap.md
+++ b/docs/Roadmap.md
@@ -406,7 +406,7 @@ Anwendung stattdessen in ihr eigenes Installationsverzeichnis — das ein Update
überschreibt. Der Befund blockiert damit denselben Betrieb, für den #214 und #215 da
sind, und muss vor ihnen weg.
-Die Pflichtliste des [Oberflächen-Leitfadens](Oberflaechen-Leitfaden.md) führt genau
+Die Pflichtliste des [Oberflächen-Leitfadens](archiv/Oberflaechen-Leitfaden.md) führt genau
diesen Fall: „Keine relativen Pfade … zeigt unter Linux ins Leere."
Vorgabe für die Umsetzung: Ein relativer Wert wird gegen `instance.WorkingDirectory`
@@ -885,7 +885,7 @@ Wer einen Punkt anfasst, liest das zugehörige Dokument zuerst.
| Dokument | Wofür |
|---|---|
-| [Oberflaechen-Leitfaden](Oberflaechen-Leitfaden.md) | Arbeiten an `ClawdDotNet.Desktop`: Schichtschnitt, Fäden, Avalonia-12-Fallen, Prüfliste. Die Oberfläche hat **keine** Testabdeckung — Abschnitt 4 ist deshalb Pflichtlektüre vor jeder Abgabe |
+| [Oberflaechen-Leitfaden](archiv/Oberflaechen-Leitfaden.md) | **Archiv seit #764** (Fenster entfernt). Damals: Arbeiten an `ClawdDotNet.Desktop`: Schichtschnitt, Fäden, Avalonia-12-Fallen, Prüfliste. Die Oberfläche hat **keine** Testabdeckung — Abschnitt 4 ist deshalb Pflichtlektüre vor jeder Abgabe |
| [ToolDevelopmentGuide](ToolDevelopmentGuide.md) | Ein neues Agenten-Tool bauen |
| [InstanceSetupGuide](InstanceSetupGuide.md) | Eine Instanz einrichten |
| [Teststrategie](Teststrategie.md) | Was wie getestet wird, Fallkatalog |
diff --git a/docs/WebUI-Leitfaden.md b/docs/WebUI-Leitfaden.md
index 6001679..d2bb5ea 100644
--- a/docs/WebUI-Leitfaden.md
+++ b/docs/WebUI-Leitfaden.md
@@ -4,7 +4,7 @@ Für die zweite Oberfläche von ClawdDotNet: eine Weboberfläche im Browser, neb
der Avalonia-Oberfläche und gleichzeitig mit ihr.
Geschrieben für **Gemini 3.8 Flash**, das die Oberfläche baut. Stand: 2026-09-06.
-Gegenstück zum [Oberflächen-Leitfaden](Oberflaechen-Leitfaden.md), der für
+Gegenstück zum [Oberflächen-Leitfaden](archiv/Oberflaechen-Leitfaden.md) (seit #764 im Archiv), der für
Avalonia gilt.
Vorhaben im Deploymentcenter:
diff --git a/docs/Oberflaechen-Leitfaden.md b/docs/archiv/Oberflaechen-Leitfaden.md
similarity index 97%
rename from docs/Oberflaechen-Leitfaden.md
rename to docs/archiv/Oberflaechen-Leitfaden.md
index d46d122..803a050 100644
--- a/docs/Oberflaechen-Leitfaden.md
+++ b/docs/archiv/Oberflaechen-Leitfaden.md
@@ -1,5 +1,9 @@
# Oberfläche — Leitfaden
+> **Archiv seit 2026-10-02 (#764, Stufe 3).** Die Avalonia-Fenster, für die dieser Leitfaden
+> galt, sind entfernt. Die einzige Oberfläche ist die WebUI ([WebUI-Leitfaden](../WebUI-Leitfaden.md)).
+> Aufbewahrt wegen der Begründungen darin.
+
Für alle, die an `src/ClawdDotNet.Desktop` arbeiten.
Stand: 2026-08-23.
diff --git a/docs/umsetzungsplaene/WebUI-Only-und-Englisch.md b/docs/umsetzungsplaene/WebUI-Only-und-Englisch.md
index 218826f..2d35668 100644
--- a/docs/umsetzungsplaene/WebUI-Only-und-Englisch.md
+++ b/docs/umsetzungsplaene/WebUI-Only-und-Englisch.md
@@ -134,6 +134,17 @@ Probelauf am selben Tag mit `CefGlue.Next.Avalonia` 152.7977.831 (CEF 152, net10
### Stufe 3 — Avalonia-Fenster entfernen (Flash: mit Vorsicht)
+**Stand 2026-10-02: erledigt, vor Stufe 2 gezogen.** Der Mensch hat so entschieden, weil CEF
+Avalonia 11.3 braucht und der Instanzprozess gar kein Avalonia mehr laden soll. Vorher
+nachgezogen, weil Stufe 0 sie übersehen hatte und es sie nur im Fenster gab:
+Telegram-Client-Anmeldung (#794) und Update anbieten/einspielen (#795, im Manager; unter systemd
+über `clawddotnet-update.service`). `ClawdDotNet` ohne Schalter startet jetzt den Manager (unter
+systemd über den Dienst, nie doppelt) und öffnet die WebUI im Systembrowser, bis die Hülle aus
+Stufe 2 das übernimmt. `instanceWindows` in `manager.json` wird gelesen, aber nicht mehr
+ausgewertet; die Kette „Fenster nach vorn holen" ist entfernt. Das Linux-Paket hat 38,8 MB,
+mit `systemd/`-Vorlagen, und ist auf der VM Ende-zu-Ende geprüft (Instanz anlegen, kopflos
+starten, anhalten).
+
- `Views/`, `Controls/`, `Converters/`, ViewModels der Fenster, `App.axaml` — weg.
- Avalonia-Pakete aus der `.csproj`, soweit die Hülle sie nicht braucht.
- `Program.cs`: ohne `--headless`/`--managed`/MCP-Brücke startet künftig die Hülle.
@@ -264,3 +275,5 @@ Stand 2026-10-02, erhoben gegen `main` (c03d6d0). Quelle je Zeile: die `[RelayCo
| L9 | Agent „jetzt laufen lassen" | nein, Endpunkt da | nein |
| L10 | Automatische Sicherung einstellen | ja | nein |
| L11 | Sicherung hochladen / als neue Instanz zurückspielen — vom Menschen am 2026-10-02 für Stufe 1 bestätigt | ja | Upload → ja |
+| L12 | Telegram-Client-Anmeldung (Code, 2FA) — in Stufe 0 übersehen, gefunden in Stufe 3 | ja → **#794** | ja (Zugangsdaten) |
+| L13 | Update anbieten und einspielen — in Stufe 0 übersehen, gefunden in Stufe 3 | ja → **#795** | Update-Agent → ja |
diff --git a/src/ClawdDotNet.App/AppHost.cs b/src/ClawdDotNet.App/AppHost.cs
index 97e7365..43de8c7 100644
--- a/src/ClawdDotNet.App/AppHost.cs
+++ b/src/ClawdDotNet.App/AppHost.cs
@@ -306,14 +306,10 @@ public sealed class AppHost : IAsyncDisposable
///
public bool IsManaged { get; init; }
- ///
- /// Telegram-Anmeldecode und 2FA-Passwort. Null lässt die MTProto-Anmeldung aus —
- /// im kopflosen Betrieb der richtige Weg, weil ein Eingabefenster dort einen
- /// Dienst dauerhaft blockieren würde.
- ///
- public Func>? TelegramLogin { get; init; }
-
- public Func>? Telegram2FA { get; init; }
+ // #794: Hier standen TelegramLogin und Telegram2FA — Rückrufe für einen Eingabedialog.
+ // Null liess die MTProto-Anmeldung aus, im kopflosen Betrieb der richtige Weg, weil
+ // ein Eingabefenster dort einen Dienst dauerhaft blockieren würde. Die Anmeldung läuft
+ // jetzt über die Weboberfläche (TelegramLoginBroker) und blockiert nichts.
}
///
@@ -496,7 +492,7 @@ public sealed class AppHost : IAsyncDisposable
host._shutdown.Add(host.LicenseWatch.DisposeAsync);
}
- await host.ConnectTelegramAsync(callbacks, logger);
+ host.ConnectTelegram(logger);
await host.StartDeploymentcenterAsync(loggerFactory, ct);
// Die Oberflaeche bietet die taegliche Sicherung an und schreibt Uhrzeit und
@@ -1068,35 +1064,20 @@ public sealed class AppHost : IAsyncDisposable
return AdminResult.Success("Telegram login started.");
}
- private async Task ConnectTelegramAsync(Callbacks callbacks, ILogger logger)
+ ///
+ /// #794: The Telegram login runs through the web interface, in the background, so the HTTP
+ /// interface the code has to come in through is up first. Until #764 a desktop dialog asked
+ /// here and the start waited for it; that window is gone.
+ ///
+ private void ConnectTelegram(ILogger logger)
{
if (TelegramClient is null) return;
- // #794: Without a dialog (every instance without a window), the login runs through
- // the web interface - in the background, so the HTTP interface it needs comes up.
- if (callbacks.TelegramLogin is null)
- {
- _telegramLogger = logger;
- TelegramLogin = new TelegramLoginBroker(configured: true);
- TelegramClient.OnLoginCodeRequired += prompt => TelegramLogin.AskForCodeAsync(prompt);
- TelegramClient.On2FAPasswordRequired += () => TelegramLogin.AskForPasswordAsync();
- StartTelegramLogin();
- return;
- }
-
- TelegramClient.OnLoginCodeRequired += prompt => callbacks.TelegramLogin(prompt);
-
- if (callbacks.Telegram2FA is not null)
- TelegramClient.On2FAPasswordRequired += () => callbacks.Telegram2FA();
-
- try
- {
- await TelegramClient.ConnectAsync(CancellationToken.None);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Telegram: Login fehlgeschlagen");
- }
+ _telegramLogger = logger;
+ TelegramLogin = new TelegramLoginBroker(configured: true);
+ TelegramClient.OnLoginCodeRequired += prompt => TelegramLogin.AskForCodeAsync(prompt);
+ TelegramClient.On2FAPasswordRequired += () => TelegramLogin.AskForPasswordAsync();
+ StartTelegramLogin();
}
///
diff --git a/src/ClawdDotNet.Desktop/App.axaml b/src/ClawdDotNet.Desktop/App.axaml
deleted file mode 100644
index 4b234d1..0000000
--- a/src/ClawdDotNet.Desktop/App.axaml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/App.axaml.cs b/src/ClawdDotNet.Desktop/App.axaml.cs
deleted file mode 100644
index 4102e1d..0000000
--- a/src/ClawdDotNet.Desktop/App.axaml.cs
+++ /dev/null
@@ -1,995 +0,0 @@
-using System.Diagnostics;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Markup.Xaml;
-using Avalonia.Threading;
-using Avalonia.Layout;
-using Avalonia.Media;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.App.Settings;
-using ClawdDotNet.Desktop.Services;
-using ClawdDotNet.Desktop.ViewModels;
-using ClawdDotNet.Desktop.Views;
-using ClawdDotNet.Web;
-using ClawdDotNet.Core.Instances;
-using ClawdDotNet.Core.Storage;
-using Microsoft.Extensions.Logging;
-
-namespace ClawdDotNet.Desktop;
-
-public partial class App : Application
-{
- ///
- /// Schalter der Befehlszeile, mit dem sich ClawdDotNet auf eine bestimmte Instanz
- /// starten laesst. Gesetzt wird er nur von der Anwendung selbst, wenn der
- /// Instanzdialog einen Wechsel verlangt — deshalb ist er nirgends dokumentiert.
- ///
- private const string InstanceArgument = CommandLine.InstanceArgument;
-
- private AppHost? _host;
-
- /// Die Weboberflaeche, falls sie eingeschaltet ist. Sonst null.
- private InstanceHttpHost? _web;
-
- /// Vorgewaehlte Instanz aus der Befehlszeile; wird genau einmal verbraucht.
- private string? _preselectedInstancePath;
-
- ///
- /// Vom Instanzmanager gestartet (--managed).
- ///
- /// Dann gehoert der Instanzwechsel dorthin: Der Manager haelt die Fenster aller
- /// Instanzen im Hintergrund und holt das gewuenschte nach vorn (E-M2). Ein Fenster,
- /// das sich selbst mit einer anderen Instanz neu startet, naehme genau die Instanz
- /// mit, aus der heraus jemand wechselt.
- ///
- private bool _managed;
-
- // #576: Muessen leben, solange die Anwendung laeuft - eine eingesammelte
- // Registrierung meldet sich ab.
- private System.Runtime.InteropServices.PosixSignalRegistration? _sigterm;
- private System.Runtime.InteropServices.PosixSignalRegistration? _sigint;
-
- ///
- /// SIGTERM und SIGINT beenden auch mit Fenster geordnet (#576).
- ///
- /// Kopflos war das seit HeadlessRun verdrahtet, mit Fenster nicht: Unter Linux
- /// reagierte die Instanz auf SIGTERM gar nicht und wurde nach Ablauf jeder Frist hart
- /// abgeschossen - beim Abmelden, bei systemctl stop, beim Herunterfahren. Derselbe Weg
- /// wie die Bitte des Instanzmanagers: das Fenster ohne Rueckfrage schliessen, das
- /// geordnete Ende laeuft von dort.
- ///
- private void RegisterSignals(IClassicDesktopStyleApplicationLifetime desktop)
- {
- void Beenden(System.Runtime.InteropServices.PosixSignalContext context)
- {
- // Abfangen, nicht durchlassen: Sonst raeumte die Laufzeit den Prozess ab,
- // waehrend das geordnete Ende noch laeuft.
- context.Cancel = true;
- var signal = context.Signal.ToString();
-
- Dispatcher.UIThread.Post(() =>
- {
- _host?.LoggerFactory.CreateLogger("ClawdDotNet.Shutdown")
- .LogInformation("Signal {Signal} empfangen - die Instanz endet geordnet.", signal);
-
- if (_host is null)
- desktop.Shutdown();
- else if (desktop.MainWindow is MainWindow fenster)
- fenster.BeendenOhneRueckfrage();
- else
- _ = BeendeGeordnetAsync(desktop, signal);
- });
- }
-
- try
- {
- _sigterm = System.Runtime.InteropServices.PosixSignalRegistration.Create(
- System.Runtime.InteropServices.PosixSignal.SIGTERM, Beenden);
- _sigint = System.Runtime.InteropServices.PosixSignalRegistration.Create(
- System.Runtime.InteropServices.PosixSignal.SIGINT, Beenden);
- }
- catch (PlatformNotSupportedException)
- {
- // Ohne Signale bleibt es beim Schliessen ueber Fenster und Manager.
- }
- }
-
- public override void Initialize() => AvaloniaXamlLoader.Load(this);
-
- public override void OnFrameworkInitializationCompleted()
- {
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- {
- // Erst beenden, wenn wir es sagen: Zwischen Instanzauswahl und Hauptfenster
- // ist kurz gar kein Fenster offen. Mit OnLastWindowClose würde die Anwendung
- // in genau dieser Lücke aussteigen.
- desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
-
- _preselectedInstancePath = CommandLine.ReadInstancePath(desktop.Args);
- _managed = CommandLine.IsManaged(desktop.Args);
-
- ApplySavedThemeEarly();
- ApplySavedLanguageEarly();
-
- desktop.ShutdownRequested += async (_, _) =>
- {
- // Reihenfolge: erst die Weboberflaeche, dann der Aufbau. Andersherum
- // bediente Kestrel noch Anfragen gegen eine bereits geschlossene
- // Datenbank.
- await ShutdownWebAsync();
- if (_host is not null) await _host.DisposeAsync();
- };
-
- RegisterSignals(desktop);
-
- // Nicht abwarten: OnFrameworkInitializationCompleted muss zurückkehren,
- // damit die Nachrichtenschleife anläuft — sonst gäbe es keinen Faden, auf
- // dem die Fenster des Startvorgangs überhaupt erscheinen könnten.
- _ = StartAsync(desktop);
- }
-
- base.OnFrameworkInitializationCompleted();
- }
-
- ///
- /// Setzt das gespeicherte Farbschema, bevor das erste Fenster erscheint.
- ///
- /// Das Ansichtsmodell des Hauptfensters tut dasselbe — aber erst, wenn der
- /// Aufbau steht. Dazwischen liegt die Instanzauswahl, und die waere sonst bei einem
- /// Benutzer im Dunkelmodus hell und der Rest der Anwendung dunkel.
- ///
- /// Ein zweites Laden der Einstellungen ist unbedenklich: Load liest nur und
- /// schreibt allenfalls beim allerersten Start die Vorgaben fest.
- ///
- private static void ApplySavedThemeEarly()
- {
- try
- {
- var settings = new SettingsManager();
- settings.Load();
- ThemeManager.Apply(ThemeManager.IsDarkName(settings.AppSettings.Theme));
- }
- catch (Exception)
- {
- // Ohne lesbare Einstellungen bleibt es beim hellen Schema.
- }
- }
-
- ///
- /// Setzt die gespeicherte Sprache, bevor das erste Fenster erscheint.
- ///
- private static void ApplySavedLanguageEarly()
- {
- try
- {
- var settings = new SettingsManager();
- settings.Load();
- LocalizationManager.Instance.SetLanguage(settings.AppSettings.Language);
- }
- catch (Exception)
- {
- LocalizationManager.Instance.SetLanguage(LocalizationManager.DefaultLanguage);
- }
- }
-
- private async Task StartAsync(IClassicDesktopStyleApplicationLifetime desktop)
- {
- var result = await AppHost.StartAsync(new AppHost.Callbacks
- {
- SelectInstance = SelectInstanceAsync,
- License = new AvaloniaLicensePrompt(),
- IsManaged = _managed,
- TelegramLogin = prompt => AskAsync("Telegram Verifizierung", prompt),
- Telegram2FA = () => AskAsync("Telegram 2FA", "Bitte 2FA-Passwort eingeben:")
- });
-
- if (result.Error is { } error)
- {
- await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Fehler", error);
- desktop.Shutdown(1);
- return;
- }
-
- if (result.Host is null)
- {
- // Abbruch durch den Benutzer oder fehlende Lizenz — beides ist bereits
- // erklärt worden, hier kommt keine weitere Meldung hinterher.
- desktop.Shutdown();
- return;
- }
-
- _host = result.Host;
- LocalizationManager.Instance.SetLanguage(_host.Settings.AppSettings.Language);
-
- HookErrorReporting(_host);
- HookLicenseWatch(_host, desktop);
-
- // Nach dem Aufbau und vor dem Fenster: Die HTTP-Flaeche braucht den fertigen
- // AppHost, und ein belegter Port darf das Fenster nicht aufhalten — TryStart
- // gibt in jedem Fehlerfall null zurueck und schreibt den Grund ins Protokoll.
- //
- // Sie ist mehr als die Weboberflaeche: Immer dabei ist der Socket im
- // Instanzverzeichnis, ueber den der Instanzmanager die Instanz erreicht (#528).
- _web = InstanceHttpHost.TryStart(_host, _host.LoggerFactory);
-
- var mainViewModel = new MainWindowViewModel(_host)
- {
- // Wer beaufsichtigt wird, wechselt nicht selbst. Der geprüfte Befund des
- // Aufbaus, nicht der Schalter der Befehlszeile (#535): Wer --managed ohne
- // gueltige Unterschrift setzt, ist eine eigenstaendige Instanz und darf
- // wechseln wie jede andere.
- CanSwitchInstance = !_host.IsManaged
- };
-
- // Der Wechsel gehoert hierher und nicht ins Fenster: Er braucht den Aufbau und
- // die Lebensdauer der Anwendung, und beides kennt nur diese Klasse.
- mainViewModel.RequestInstanceManagerDialog += () => _ = SwitchInstanceAsync(desktop);
-
- desktop.MainWindow = new MainWindow { DataContext = mainViewModel };
-
- // Der Manager bittet ueber den Socket, dieses Fenster nach vorn zu holen (E-M2).
- if (_web is not null)
- {
- _web.Control.FocusRequested += () => BringToFront(desktop);
-
- // Und er bittet, zu enden. Ohne diese Zeile lief die Bitte ins Leere: Der
- // Endpunkt nahm sie an, antwortete mit 202 - und niemand hoerte zu. Der
- // Manager wartete seine Frist ab und schoss den Prozess dann jedes Mal ab.
- // Kopflos war das verdrahtet (HeadlessRun), mit Fenster nicht; und mit
- // Fenster ist die Vorgabe (ManagerSettings.InstanceWindows).
- _web.Control.ShutdownRequested += () => Dispatcher.UIThread.Post(() =>
- {
- if (desktop.MainWindow is MainWindow fenster)
- fenster.BeendenOhneRueckfrage();
- else
- _ = BeendeGeordnetAsync(desktop, "Instanzmanager");
- });
- }
-
- desktop.MainWindow.Show();
- desktop.MainWindow.Closed += (_, _) => _ = BeendeGeordnetAsync(desktop, "Fenster");
-
- await OfferUpdateAsync(_host, desktop);
- }
-
- ///
- /// Meldet ungefangene Ausnahmen an den Fehler-Stream des Deploymentcenters.
- ///
- /// Erst hier verdrahtet, nicht in Main: Vor dem Aufbau gibt es weder
- /// Einstellungen noch Token, und ohne die wäre der Meldeweg ohnehin der Leerlauf.
- /// Die Kehrseite ist bewusst in Kauf genommen — ein Absturz während des
- /// Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.
- ///
- private static void HookErrorReporting(AppHost host)
- {
- AppDomain.CurrentDomain.UnhandledException += (_, args) =>
- {
- if (args.ExceptionObject is Exception ex)
- {
- // Der Prozess endet gleich: kurze Frist, dann weiterlaufen lassen.
- host.Errors.ReportAsync(ex, fatal: args.IsTerminating)
- .Wait(TimeSpan.FromSeconds(3));
- }
- };
-
- TaskScheduler.UnobservedTaskException += (_, args) =>
- {
- _ = host.Errors.ReportAsync(args.Exception, fatal: false);
-
- // Ohne Observe reißt eine unbeobachtete Ausnahme in manchen Konfigurationen
- // den Prozess mit — und das wäre eine Nebenwirkung des Meldens.
- args.SetObserved();
- };
-
- Dispatcher.UIThread.UnhandledException += (_, args) =>
- {
- _ = host.Errors.ReportAsync(args.Exception, fatal: false);
-
- // Ein Fehler in einem Ereignisbehandler soll die Oberfläche nicht beenden.
- args.Handled = true;
- };
- }
-
- /// Ein Widerruf beendet die Anwendung, ohne Beenden-Rückfrage.
- private static void HookLicenseWatch(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
- {
- if (host.LicenseWatch is null) return;
-
- host.LicenseWatch.Revoked += async message =>
- {
- await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Lizenz", message);
- await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(2));
- };
- }
-
- ///
- /// Fragt, ob ein verfügbares Update jetzt eingespielt werden soll.
- ///
- /// Bewusst eine Frage und kein Selbstlauf: Wann aktualisiert wird, entscheidet
- /// der Benutzer — eine Anwendung, die sich beim Start selbst beendet, um sich zu
- /// erneuern, ist genau dann im Weg, wenn man sie braucht.
- ///
- private async Task OfferUpdateAsync(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
- {
- if (host.Deploymentcenter is null) return;
-
- // Die Prüfung läuft nebenher; kurz Zeit geben, dann aufgeben.
- for (var waited = 0; host.Deploymentcenter.Update is null && waited < 10; waited++)
- await Task.Delay(TimeSpan.FromSeconds(1));
-
- if (host.Deploymentcenter.Update is not { IsAvailable: true } update) return;
-
- var notes = string.IsNullOrWhiteSpace(update.ReleaseNotes) ? "" : $"\n\n{update.ReleaseNotes}";
-
- var install = await ConfirmAsync(
- update.IsCritical ? "ClawdDotNet – Wichtiges Update" : "ClawdDotNet – Update",
- $"Version {update.LatestVersion} ist verfügbar.{notes}\n\n"
- + "Zum Einspielen wird ClawdDotNet beendet und danach neu gestartet.",
- confirmText: "Jetzt installieren",
- cancelText: "Später");
-
- if (!install) return;
-
- await StartUpdateAsync(host, desktop, update.LatestVersion);
- }
-
- ///
- /// Die Reihenfolge ist der ganze Punkt dieser Methode.
- ///
- ///
- /// - Dem Watchdog sagen, dass gleich ein Update kommt — sonst sieht das
- /// Verschwinden der Instanz aus wie ein Ausfall.
- /// - Geordnet herunterfahren: Datenbank, Scanner, Telegram, Abmeldung.
- /// - Erst dann den Agenten starten. Er bekommt unsere Prozesskennung und wartet,
- /// bis wir weg sind, statt über gesperrte Dateien zu kopieren.
- /// - Selbst beenden.
- ///
- ///
- /// Die Verlockung wäre, LaunchUpdateAgent das Beenden zu überlassen —
- /// das tut es aber über Environment.Exit und übergeht damit Schritt 2
- /// vollständig.
- ///
- private async Task StartUpdateAsync(
- AppHost host, IClassicDesktopStyleApplicationLifetime desktop, string version)
- {
- host.Deploymentcenter!.AnnounceUpdate(version);
-
- var settings = host.Settings.AppSettings;
-
- // Vor dem Aufbau: Kestrel darf keine Anfrage mehr gegen eine geschlossene
- // Datenbank bedienen — und schon gar nicht, waehrend der update-agent
- // gleich die Dateien darunter austauscht.
- await ShutdownWebAsync();
- await host.DisposeAsync();
-
- var problem = host.Deploymentcenter.StartUpdate(settings, AppHost.AppVersion);
-
- if (problem is not null)
- {
- await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Update", problem);
-
- // Der Aufbau ist bereits abgebaut — weiterarbeiten wäre nicht ehrlich.
- await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(1));
- return;
- }
-
- await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
- }
-
- /// Ja/Nein-Rückfrage. Avalonia bringt kein MessageBox mit.
- private static async Task ConfirmAsync(
- string title, string message, string confirmText, string cancelText)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var completion = new TaskCompletionSource();
-
- var confirm = new Button { Content = confirmText, IsDefault = true };
- var cancel = new Button { Content = cancelText, IsCancel = true };
-
- var window = new Window
- {
- Title = title,
- Width = 480,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Content = new StackPanel
- {
- Margin = new Thickness(20),
- Spacing = 16,
- Children =
- {
- new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
- new StackPanel
- {
- Orientation = Avalonia.Layout.Orientation.Horizontal,
- HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
- Spacing = 8,
- Children = { cancel, confirm }
- }
- }
- }
- };
-
- var answer = false;
- confirm.Click += (_, _) => { answer = true; window.Close(); };
- cancel.Click += (_, _) => window.Close();
-
- // Schließt der Benutzer das Fenster, gilt das als „Später".
- window.Closed += (_, _) => completion.TrySetResult(answer);
-
- window.Show();
- return await completion.Task;
- });
-
- ///
- /// Holt das Fenster dieser Instanz nach vorn — auf Bitte des Instanzmanagers.
- ///
- /// Die Instanz macht das selbst, statt dass jemand von aussen an ihrem
- /// Fenster zieht. Der Manager kennt kein Fenstersystem und soll keines kennen; er
- /// bittet über den Socket, und wer ein Fenster hat, holt es nach vorn. Kopflos läuft
- /// die Bitte ins Leere, und das ist die richtige Antwort.
- ///
- /// Das letzte Wort hat der Fenstermanager. Windows lässt einen Prozess
- /// im Hintergrund den Vordergrund nicht einfach an sich reissen — Activate
- /// blinkt dann nur in der Leiste. Der kurze Ausflug über Topmost bringt es in
- /// den meisten Fällen trotzdem nach vorn; unter Linux entscheidet der Fenstermanager,
- /// und manche sind da streng. Mehr als bitten kann eine Anwendung nicht, und mehr
- /// sollte sie auch nicht tun.
- ///
- private static void BringToFront(IClassicDesktopStyleApplicationLifetime desktop)
- => Dispatcher.UIThread.Post(() =>
- {
- if (desktop.MainWindow is not { } fenster) return;
-
- if (fenster.WindowState == WindowState.Minimized)
- fenster.WindowState = WindowState.Normal;
-
- fenster.Show();
-
- var vorher = fenster.Topmost;
- fenster.Topmost = true;
- fenster.Topmost = vorher;
-
- fenster.Activate();
- });
-
- ///
- /// Instanzwechsel aus der laufenden Anwendung.
- ///
- /// Ein Wechsel im laufenden Prozess ist nicht vorgesehen und waere auch keine
- /// kleine Sache: Engine, Scanner, Telegram-Anbindung und Datenbank haengen alle an
- /// einer Instanz. Statt das umzubauen startet sich die Anwendung mit
- /// --instance neu — derselbe Weg, den auch das Update geht.
- ///
- /// Reihenfolge wie dort: erst geordnet herunterfahren, dann den neuen Prozess
- /// starten. Andersherum liefen kurz zwei Prozesse auf derselben Datenbank.
- ///
- private enum StartModeChoice
- {
- LaunchManager,
- Standalone,
- Cancel
- }
-
- private enum StandaloneSwitchChoice
- {
- LaunchManager,
- SwitchStandalone,
- Cancel
- }
-
- ///
- /// Instanzwechsel aus der laufenden Anwendung.
- ///
- private async Task SwitchInstanceAsync(IClassicDesktopStyleApplicationLifetime desktop)
- {
- if (_host is null) return;
-
- if (_host.IsManaged)
- {
- await new AvaloniaLicensePrompt().ShowInfoAsync(
- "ClawdDotNet – Instanzwechsel",
- "Diese Instanz wird vom Instanzmanager geführt. Gewechselt wird dort — "
- + "er holt das Fenster der gewünschten Instanz nach vorn, statt eine "
- + "laufende zu beenden.");
-
- return;
- }
-
- // Im Standalone-Modus den Benutzer fragen:
- var switchChoice = await AskStandaloneSwitchModeAsync(desktop.MainWindow);
- if (switchChoice == StandaloneSwitchChoice.LaunchManager)
- {
- LaunchInstanceManagerAndOpenBrowser();
- return;
- }
- if (switchChoice == StandaloneSwitchChoice.Cancel)
- {
- return;
- }
-
- var viewModel = new InstancePickerViewModel(_host.Directories, _host.InstancePath);
- var window = new InstancePickerWindow { DataContext = viewModel };
-
- // Schliesst der Benutzer das Fenster, gilt das als Abbruch.
- window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);
-
- // Umgekehrt: Sobald eine Wahl feststeht, schliesst sich das Fenster von selbst.
- _ = viewModel.Result.ContinueWith(
- _ => Dispatcher.UIThread.Post(window.Close), TaskScheduler.Default);
-
- if (desktop.MainWindow is { } owner) await window.ShowDialog(owner);
- else window.Show();
-
- var path = await viewModel.Result;
-
- if (path is null) return;
- if (string.Equals(path, _host.InstancePath, StringComparison.OrdinalIgnoreCase)) return;
-
- var executable = Environment.ProcessPath;
-
- if (executable is null)
- {
- await new AvaloniaLicensePrompt().ShowErrorAsync(
- "ClawdDotNet – Instanzwechsel",
- "Der eigene Programmpfad ist nicht ermittelbar; ein Neustart ist von hier "
- + "aus nicht möglich. Bitte ClawdDotNet von Hand neu starten.");
- return;
- }
-
- // Neue Instanz mit UseShellExecute = true starten (keine geerbten Datei- oder Socket-Handles)
- try
- {
- Process.Start(new ProcessStartInfo(executable)
- {
- ArgumentList = { InstanceArgument, path },
- UseShellExecute = true
- });
- }
- catch (Exception ex)
- {
- await new AvaloniaLicensePrompt().ShowErrorAsync(
- "ClawdDotNet – Instanzwechsel",
- $"Neustart fehlgeschlagen: {ex.Message}");
- return;
- }
-
- // Hauptfenster sofort verbergen, damit es nicht eingefroren sichtbar bleibt
- if (desktop.MainWindow is { } mainWin)
- {
- mainWin.Hide();
- }
-
- await ShutdownWebAsync();
-
- var hostToDispose = _host;
- _host = null;
- if (hostToDispose is not null)
- {
- await hostToDispose.DisposeAsync();
- }
-
- // Sauber beenden
- await Dispatcher.UIThread.InvokeAsync(() =>
- {
- desktop.Shutdown(0);
- Environment.Exit(0);
- });
- }
-
- private static void LaunchInstanceManagerAndOpenBrowser()
- {
- var exePath = FindInstanceManagerExecutable();
- if (exePath is null)
- {
- _ = new AvaloniaLicensePrompt().ShowErrorAsync(
- "ClawdDotNet – Instanzmanager",
- "Die Programmdatei des Instanzmanagers (ClawdDotNet.Instanzmanager.exe) wurde nicht gefunden.");
- return;
- }
-
- try
- {
- Process.Start(new ProcessStartInfo(exePath)
- {
- UseShellExecute = true
- });
-
- SystemShell.OpenUrl($"http://localhost:{ManagerPort()}/#/instanzen");
- }
- catch (Exception ex)
- {
- _ = new AvaloniaLicensePrompt().ShowErrorAsync(
- "ClawdDotNet – Instanzmanager",
- $"Konnte Instanzmanager nicht starten: {ex.Message}");
- }
- }
-
- ///
- /// Sucht die Programmdatei des Instanzmanagers.
- ///
- /// Die Pfade werden aus Segmenten gebaut, nicht als Zeichenkette. Hier
- /// stand @"..\..\..\..\ClawdDotNet.ManagerApp\bin\Debug\net10.0" — unter Linux,
- /// dem einzigen Zielbetrieb, ist das kein Pfad, sondern ein einziger Dateiname mit
- /// Rückstrichen darin, und die Suche ging dort nie auf.
- ///
- /// Die Entwicklungspfade bleiben, weil im Betrieb beide Programme
- /// nebeneinanderliegen und der erste Versuch trifft; sie kosten dort nur zwei
- /// vergebliche File.Exists.
- ///
- private static string? FindInstanceManagerExecutable()
- {
- var exeName = OperatingSystem.IsWindows()
- ? "ClawdDotNet.Instanzmanager.exe"
- : "ClawdDotNet.Instanzmanager";
-
- var nebenan = Path.Combine(AppContext.BaseDirectory, exeName);
- if (File.Exists(nebenan)) return nebenan;
-
- string[][] entwicklungspfade =
- [
- ["..", "..", "..", "..", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"],
- ["..", "..", "..", "..", "..", "src", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"]
- ];
-
- foreach (var segmente in entwicklungspfade)
- {
- var pfad = Path.GetFullPath(Path.Combine(
- [AppContext.BaseDirectory, .. segmente, exeName]));
-
- if (File.Exists(pfad)) return pfad;
- }
-
- return null;
- }
-
- ///
- /// Der Port, auf dem der Instanzmanager seine Oberfläche anbietet.
- ///
- /// Aus manager.json gelesen und nicht fest eingetragen: Wer den Port
- /// dort ändert, bekäme sonst einen Browser auf eine Adresse, an der nichts
- /// antwortet. Diese Anwendung kennt den Typ hinter der Datei nicht — sie liest das
- /// eine Feld, das sie braucht, und nimmt bei jedem Zweifel die Vorgabe.
- ///
- private static int ManagerPort()
- {
- const int vorgabe = 7000;
-
- try
- {
- var datei = Path.Combine(AppPaths.ConfigDirectory, "manager.json");
- if (!File.Exists(datei)) return vorgabe;
-
- using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllText(datei));
-
- return doc.RootElement.TryGetProperty("webUiPort", out var port)
- && port.ValueKind == System.Text.Json.JsonValueKind.Number
- && port.TryGetInt32(out var wert)
- && wert is > 0 and < 65536
- ? wert
- : vorgabe;
- }
- catch (Exception)
- {
- // Eine unlesbare Managerdatei ist kein Grund, den Browser gar nicht zu oeffnen.
- return vorgabe;
- }
- }
-
- private static async Task AskStandaloneSwitchModeAsync(Window? owner)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var completion = new TaskCompletionSource();
- var l = LocalizationManager.Instance;
-
- var managerBtn = new Button
- {
- Content = l["desktop.startMode.managerBtn"],
- Classes = { "primary" }
- };
-
- var switchBtn = new Button
- {
- Content = l["desktop.standaloneSwitch.switchAnyway"]
- };
-
- var cancelBtn = new Button
- {
- Content = l["common.cancel"],
- IsCancel = true
- };
-
- var window = new Window
- {
- Title = l["desktop.standaloneSwitch.title"],
- Width = 540,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Icon = owner?.Icon,
- Content = new StackPanel
- {
- Margin = new Thickness(20),
- Spacing = 16,
- Children =
- {
- new TextBlock
- {
- Text = l["desktop.standaloneSwitch.warning"],
- Classes = { "h1" }
- },
- new TextBlock
- {
- Text = l["desktop.standaloneSwitch.text"],
- TextWrapping = TextWrapping.Wrap
- },
- new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Spacing = 10,
- HorizontalAlignment = HorizontalAlignment.Right,
- Children = { cancelBtn, switchBtn, managerBtn }
- }
- }
- }
- };
-
- managerBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StandaloneSwitchChoice.LaunchManager);
- window.Close();
- };
-
- switchBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StandaloneSwitchChoice.SwitchStandalone);
- window.Close();
- };
-
- cancelBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StandaloneSwitchChoice.Cancel);
- window.Close();
- };
-
- window.Closed += (_, _) => completion.TrySetResult(StandaloneSwitchChoice.Cancel);
-
- if (owner is not null)
- await window.ShowDialog(owner);
- else
- window.Show();
-
- return await completion.Task;
- });
-
- private static async Task AskStartModeAsync()
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var completion = new TaskCompletionSource();
- var l = LocalizationManager.Instance;
-
- var managerBtn = new Button
- {
- Content = l["desktop.startMode.managerBtn"],
- Classes = { "primary" }
- };
-
- var standaloneBtn = new Button
- {
- Content = l["desktop.startMode.standaloneBtn"]
- };
-
- var cancelBtn = new Button
- {
- Content = l["common.cancel"],
- IsCancel = true
- };
-
- var window = new Window
- {
- Title = l["desktop.startMode.title"],
- Width = 540,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Content = new StackPanel
- {
- Margin = new Thickness(22),
- Spacing = 16,
- Children =
- {
- new TextBlock
- {
- Text = l["desktop.startMode.question"],
- Classes = { "h1" }
- },
- new TextBlock
- {
- Text = l["desktop.startMode.explanation"],
- TextWrapping = TextWrapping.Wrap
- },
- new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Spacing = 10,
- HorizontalAlignment = HorizontalAlignment.Right,
- Children = { cancelBtn, standaloneBtn, managerBtn }
- }
- }
- }
- };
-
- managerBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StartModeChoice.LaunchManager);
- window.Close();
- };
-
- standaloneBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StartModeChoice.Standalone);
- window.Close();
- };
-
- cancelBtn.Click += (_, _) =>
- {
- completion.TrySetResult(StartModeChoice.Cancel);
- window.Close();
- };
-
- window.Closed += (_, _) => completion.TrySetResult(StartModeChoice.Cancel);
-
- window.Show();
- return await completion.Task;
- });
-
- private int _beendetSich;
-
- ///
- /// Das geordnete Ende dieser Instanz — der eine Weg, egal wer darum gebeten hat.
- ///
- /// Erst auslaufen lassen, dann abschneiden. Laufende Agenten und
- /// beanspruchte Aufgaben bekommen ihre Frist
- /// (), bevor Weboberfläche und Aufbau fallen. Ohne
- /// das endete eine Aufgabe mitten in ProcessAsync und bliebe bis zum Ablauf
- /// ihres Lease auf running stehen.
- ///
- /// Und nicht im Rückruf von desktop.ShutdownRequested. Avalonia
- /// wartet dort nicht auf async-Behandlungen; was länger dauert als ein
- /// Wimpernschlag, läuft gegen einen Prozess, der schon weg ist. Deshalb erst
- /// aufräumen und dann Shutdown rufen — der Rückruf dort bleibt als
- /// zweites Schloss stehen und tut dank der Sperren beim zweiten Mal nichts.
- ///
- private async Task BeendeGeordnetAsync(
- IClassicDesktopStyleApplicationLifetime desktop, string grund)
- {
- if (Interlocked.Exchange(ref _beendetSich, 1) != 0) return;
-
- var host = _host;
-
- if (host is not null)
- {
- host.LoggerFactory.CreateLogger("ClawdDotNet.Shutdown")
- .LogInformation("Beendigung angefordert ({Grund}).", grund);
-
- try { await host.Shutdown.BeginAsync(); }
- catch (Exception) { /* Beim Beenden zaehlt, dass alle Schritte drankommen */ }
- }
-
- await ShutdownWebAsync();
-
- if (host is not null) await host.DisposeAsync();
-
- await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
- }
-
- ///
- /// Faehrt die Weboberflaeche herunter, falls sie laeuft, und merkt sich, dass sie
- /// weg ist. Mehrfach aufrufbar: Update, Instanzwechsel und das gewoehnliche
- /// Beenden nehmen denselben Weg, und der letzte von ihnen soll nicht ins Leere
- /// greifen.
- ///
- private async Task ShutdownWebAsync()
- {
- if (_web is null) return;
-
- var web = _web;
- _web = null;
- await web.DisposeAsync();
- }
-
- ///
- /// Instanzauswahl beim Start.
- ///
- private async Task SelectInstanceAsync(InstanceDirectoryManager directories)
- {
- var preselected = _preselectedInstancePath;
- _preselectedInstancePath = null;
-
- if (preselected is not null && Directory.Exists(preselected))
- return preselected;
-
- // Hier steht bewusst der Schalter und nicht der geprüfte Befund: Diese Methode
- // läuft mitten im Aufbau, bevor es einen AppHost gibt, der geprüft haben könnte.
- // Sie entscheidet auch nur, ob ein Dialog erscheint — wer --managed ohne gültige
- // Unterschrift setzt, bekommt ihn nicht und wird gleich darauf trotzdem nach
- // seiner Lizenz gefragt (#535).
- if (!_managed)
- {
- var choice = await AskStartModeAsync();
- if (choice == StartModeChoice.LaunchManager)
- {
- LaunchInstanceManagerAndOpenBrowser();
- return null;
- }
- if (choice == StartModeChoice.Cancel)
- {
- return null;
- }
- }
-
- return await ShowInstancePickerAsync(directories);
- }
-
- private static async Task ShowInstancePickerAsync(InstanceDirectoryManager directories)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var viewModel = new InstancePickerViewModel(directories);
- var window = new InstancePickerWindow { DataContext = viewModel };
-
- // Schließt der Benutzer das Fenster, gilt das als Abbruch — sonst wartete
- // der Start für immer auf eine Auswahl, die nie kommt.
- window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);
-
- window.Show();
-
- var path = await viewModel.Result;
- window.Close();
-
- return path;
- });
-
- ///
- /// Einzeilige Abfrage — ersetzt Microsoft.VisualBasic.Interaction.InputBox,
- /// das die Telegram-Anmeldung an Windows band.
- ///
- private static async Task AskAsync(string title, string prompt)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var completion = new TaskCompletionSource();
-
- var input = new TextBox();
- var ok = new Button { Content = LocalizationManager.Instance["common.ok"], IsDefault = true };
-
- var window = new Window
- {
- Title = title,
- Width = 420,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Content = new StackPanel
- {
- Margin = new Thickness(20),
- Spacing = 12,
- Children =
- {
- new TextBlock { Text = prompt, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
- input,
- ok
- }
- }
- };
-
- ok.Click += (_, _) => window.Close();
- window.Closed += (_, _) => completion.TrySetResult(input.Text?.Trim() ?? "");
-
- window.Show();
- input.Focus();
-
- return await completion.Task;
- });
-}
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Bold.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Bold.ttf
deleted file mode 100644
index 2f20cbe..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Bold.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Medium.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Medium.ttf
deleted file mode 100644
index d546e7a..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Medium.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Regular.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Regular.ttf
deleted file mode 100644
index bb3faec..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-Regular.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-SemiBold.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-SemiBold.ttf
deleted file mode 100644
index 7c974a4..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/Barlow-SemiBold.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Medium.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Medium.ttf
deleted file mode 100644
index eee49eb..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Medium.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Regular.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Regular.ttf
deleted file mode 100644
index ecbe0cc..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-Regular.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-SemiBold.ttf b/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-SemiBold.ttf
deleted file mode 100644
index 9df6657..0000000
Binary files a/src/ClawdDotNet.Desktop/Assets/Fonts/BarlowCondensed-SemiBold.ttf and /dev/null differ
diff --git a/src/ClawdDotNet.Desktop/Assets/Fonts/OFL.txt b/src/ClawdDotNet.Desktop/Assets/Fonts/OFL.txt
deleted file mode 100644
index 2f22ba6..0000000
--- a/src/ClawdDotNet.Desktop/Assets/Fonts/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow)
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj b/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj
index d26a64b..309d1ef 100644
--- a/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj
+++ b/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj
@@ -9,32 +9,14 @@
ClawdDotNet
Assets\app.ico
-
- true
-
+ WinExe verhaelt sich dort ohnehin wie Exe. #764: Seit die Fenster entfernt sind,
+ heisst das unter Windows: Der Starter (ohne Schalter) blitzt nicht als Konsole auf. -->
false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -42,13 +24,6 @@
-
-
-
- PreserveNewest
-
-
-
@@ -56,7 +31,10 @@
warum das so sein muss, steht in ClawdDotNet.Web/WebUiHost.cs.
Die Richtung des Verweises ist Absicht: Desktop kennt Web, Web kennt Desktop
- nicht. Wer hier umdreht, hat den Schnitt verletzt. -->
+ nicht. Wer hier umdreht, hat den Schnitt verletzt.
+
+ #764: Seit die Fenster entfernt sind, ist der Instanzprozess (HeadlessRun) der
+ einzige Wirt; die HTTP-Flaeche ist sein Zugang fuer Manager und Weboberflaeche. -->
diff --git a/src/ClawdDotNet.Desktop/Controls/BlueprintFrame.cs b/src/ClawdDotNet.Desktop/Controls/BlueprintFrame.cs
deleted file mode 100644
index 0f79b59..0000000
--- a/src/ClawdDotNet.Desktop/Controls/BlueprintFrame.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-
-namespace ClawdDotNet.Desktop.Controls;
-
-///
-/// Der Rahmen mit den vier Eckmarken — das wiederkehrende Motiv des Entwurfs.
-///
-/// Eigenes Steuerelement statt vier von Hand gesetzter Striche je Stelle: Die Marken
-/// liegen bewusst halb außerhalb des Rahmens, und das ist genau die Sorte Detail,
-/// die beim Nachbauen an der fünften Stelle nicht mehr stimmt.
-///
-/// Weil die Marken über den Rand hinausragen, darf kein Elternteil zuschneiden.
-/// In der Praxis heißt das: nicht direkt an den Fensterrand setzen, sondern mit etwas
-/// Abstand — die Vorlage im Entwurf tut dasselbe.
-///
-/// Sparsam verwenden. Im Entwurf tragen nur die Info-Karte und die beiden Dialoge
-/// die Marken; alle übrigen Karten sind schlichte Haarlinien-Kästen (Klasse
-/// card in Styles/Shell.axaml).
-///
-public sealed class BlueprintFrame : ContentControl
-{
- protected override Type StyleKeyOverride => typeof(BlueprintFrame);
-}
diff --git a/src/ClawdDotNet.Desktop/Controls/StrokeIcon.cs b/src/ClawdDotNet.Desktop/Controls/StrokeIcon.cs
deleted file mode 100644
index 5b31636..0000000
--- a/src/ClawdDotNet.Desktop/Controls/StrokeIcon.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Documents;
-using Avalonia.Media;
-
-namespace ClawdDotNet.Desktop.Controls;
-
-///
-/// Ein Strichsymbol aus dem Entwurf — 24×24-Geometrie, gezeichnet mit der geerbten
-/// Vordergrundfarbe.
-///
-/// Warum nicht PathIcon: Das füllt seine Geometrie. Die Symbole des Entwurfs
-/// sind Umrisse (Lucide, Strichstärke 1.5); gefüllt werden daraus schwarze Klumpen.
-///
-/// Warum nicht Path mit Stroke: Dann müsste an jeder einzelnen Stelle
-/// die Farbe mitgegeben werden — und zwar die richtige, je nachdem ob das Symbol
-/// in einer Schaltfläche, einer ausgewählten Navigationszeile oder der Statusleiste sitzt.
-/// ist hier von übernommen und erbt
-/// daher wie eine Textfarbe: Wer den Text einfärbt, färbt das Symbol mit.
-///
-/// Die Strichstärke wird beim Skalieren mitskaliert. Das ist keine Nachlässigkeit,
-/// sondern das Verhalten von SVG: stroke-width zählt in Einheiten des viewBox.
-/// Ein Symbol mit 1.5 in 24×24, dargestellt auf 16 px, hat genau 1 px starke Linien.
-///
-public sealed class StrokeIcon : Control
-{
- /// Kantenlänge des Koordinatenraums aller Symbole (SVG-viewBox 0 0 24 24).
- private const double ViewBox = 24d;
-
- public static readonly StyledProperty DataProperty =
- AvaloniaProperty.Register(nameof(Data));
-
- /// Geerbt wie eine Textfarbe — siehe Klassenbeschreibung.
- public static readonly StyledProperty ForegroundProperty =
- TextElement.ForegroundProperty.AddOwner();
-
- public static readonly StyledProperty StrokeThicknessProperty =
- AvaloniaProperty.Register(nameof(StrokeThickness), 1.5d);
-
- /// Für die wenigen massiven Symbole des Entwurfs (Wiedergabe, Halt).
- public static readonly StyledProperty IsFilledProperty =
- AvaloniaProperty.Register(nameof(IsFilled));
-
- static StrokeIcon()
- {
- AffectsRender(
- DataProperty, ForegroundProperty, StrokeThicknessProperty, IsFilledProperty);
- AffectsMeasure(DataProperty);
- }
-
- public Geometry? Data
- {
- get => GetValue(DataProperty);
- set => SetValue(DataProperty, value);
- }
-
- public IBrush? Foreground
- {
- get => GetValue(ForegroundProperty);
- set => SetValue(ForegroundProperty, value);
- }
-
- public double StrokeThickness
- {
- get => GetValue(StrokeThicknessProperty);
- set => SetValue(StrokeThicknessProperty, value);
- }
-
- public bool IsFilled
- {
- get => GetValue(IsFilledProperty);
- set => SetValue(IsFilledProperty, value);
- }
-
- ///
- /// Ohne gesetzte Größe 16×16 — die Größe, in der die meisten Symbole im Entwurf stehen.
- /// Ein unbegrenzter Vorschlag würde das Symbol den ganzen Platz nehmen lassen.
- ///
- protected override Size MeasureOverride(Size availableSize)
- {
- var width = double.IsInfinity(availableSize.Width) ? 16d : Math.Min(16d, availableSize.Width);
- var height = double.IsInfinity(availableSize.Height) ? 16d : Math.Min(16d, availableSize.Height);
- return new Size(width, height);
- }
-
- public override void Render(DrawingContext context)
- {
- if (Data is not { } geometry || Foreground is not { } brush) return;
-
- var scale = Math.Min(Bounds.Width / ViewBox, Bounds.Height / ViewBox);
- if (scale <= 0) return;
-
- // Mittig setzen, falls das Steuerelement nicht quadratisch ist.
- var offsetX = (Bounds.Width - ViewBox * scale) / 2;
- var offsetY = (Bounds.Height - ViewBox * scale) / 2;
-
- using var _ = context.PushTransform(
- Matrix.CreateScale(scale, scale) * Matrix.CreateTranslation(offsetX, offsetY));
-
- if (IsFilled)
- {
- context.DrawGeometry(brush, null, geometry);
- return;
- }
-
- // Runde Enden und Verbindungen: Ohne sie verschwinden die Punkt-Symbole
- // (Pfadstücke der Länge 0.01, etwa in „Agenten-Chats") vollständig.
- var pen = new Pen(brush, StrokeThickness,
- lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round);
-
- context.DrawGeometry(null, pen, geometry);
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Converters/IconConverter.cs b/src/ClawdDotNet.Desktop/Converters/IconConverter.cs
deleted file mode 100644
index ffc25b7..0000000
--- a/src/ClawdDotNet.Desktop/Converters/IconConverter.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Globalization;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace ClawdDotNet.Desktop.Converters;
-
-///
-/// Schlägt eine Symbol-Geometrie aus Styles/Icons.axaml über ihren Namen nach.
-///
-/// Damit halten die Ansichtsmodelle nur den Namen („IconChat") statt einer
-/// — sie brauchen dann keinen Verweis auf Avalonia.Media,
-/// und die Navigationsleiste kommt mit einer schlichten Zeichenkette je Eintrag aus.
-///
-/// Dass hier keine Themenumschaltung nötig ist, ist kein Zufall: Die Geometrien sind
-/// themenunabhängig, gefärbt wird erst beim Zeichnen über die geerbte Vordergrundfarbe
-/// (siehe Controls/StrokeIcon.cs). Für Farben wäre dieser Weg falsch — ein einmal
-/// aufgelöster Pinsel bliebe beim Wechsel auf Dunkel stehen.
-///
-public sealed class IconConverter : IValueConverter
-{
- public static readonly IconConverter Instance = new();
-
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- if (value is not string key || string.IsNullOrEmpty(key)) return null;
- if (Application.Current is not { } app) return null;
-
- return app.TryFindResource(key, out var resource) ? resource as Geometry : null;
- }
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException("Nur zur Anzeige.");
-}
-
-///
-/// Der Pfeil am Einklapp-Schalter der Seitenleiste: zeigt nach links, solange
-/// eingeklappt werden kann, und nach rechts, wenn wieder aufgeklappt wird.
-///
-public sealed class SidebarChevronConverter : IValueConverter
-{
- public static readonly SidebarChevronConverter Instance = new();
-
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- var isCollapsed = value is true;
- return IconConverter.Instance.Convert(
- isCollapsed ? "IconChevronRight" : "IconChevronLeft", targetType, parameter, culture);
- }
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException("Nur zur Anzeige.");
-}
diff --git a/src/ClawdDotNet.Desktop/Converters/LayoutConverters.cs b/src/ClawdDotNet.Desktop/Converters/LayoutConverters.cs
deleted file mode 100644
index d452b5f..0000000
--- a/src/ClawdDotNet.Desktop/Converters/LayoutConverters.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Layout;
-
-namespace ClawdDotNet.Desktop.Converters;
-
-///
-/// Richtet Sprechblasen aus: rechts fuer den Benutzer, links fuer den Agenten.
-///
-/// Ausrichtung und nicht Farbe — deshalb ist ein Konverter hier richtig. Der
-/// frueher danebenstehende ChatBubbleBrushConverter lieferte einen Pinsel mit festem
-/// Farbwert und ueberlebte den Themenwechsel nicht; die Hinterlegung eigener Nachrichten
-/// laeuft jetzt ueber die Stilklasse card accent.
-///
-public sealed class ChatAlignmentConverter : IValueConverter
-{
- public static readonly ChatAlignmentConverter Instance = new();
-
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is true ? HorizontalAlignment.Right : HorizontalAlignment.Left;
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException("Nur zur Anzeige.");
-}
-
-///
-/// Prueft, ob ein Werkzeugname mit dem ConverterParameter uebereinstimmt.
-/// Blendet im Werkzeug-Einstellungsbereich die passende Feldgruppe ein.
-///
-public sealed class ToolMatchConverter : IValueConverter
-{
- public static readonly ToolMatchConverter Instance = new();
-
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is string toolName && parameter is string targetTool
- && toolName.Equals(targetTool, StringComparison.OrdinalIgnoreCase);
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException("Nur zur Anzeige.");
-}
diff --git a/src/ClawdDotNet.Desktop/Converters/StringEqualsConverter.cs b/src/ClawdDotNet.Desktop/Converters/StringEqualsConverter.cs
deleted file mode 100644
index 36930eb..0000000
--- a/src/ClawdDotNet.Desktop/Converters/StringEqualsConverter.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace ClawdDotNet.Desktop.Converters;
-
-///
-/// Vergleicht eine Zeichenkette mit dem ConverterParameter, ohne Gross- und
-/// Kleinschreibung zu unterscheiden.
-///
-/// Gedacht fuer Classes.xyz="{Binding …}": Die Ansicht setzt damit eine
-/// Stilklasse, und die Farbe dazu steht in Styles/Shell.axaml an
-/// DynamicResource. Das ersetzt den frueheren LogLevelBrushConverter, der
-/// Pinsel mit festen Farbwerten zurueckgab — die blieben beim Wechsel auf Dunkel
-/// unveraendert stehen, weil ein einmal gelieferter Pinsel nichts mehr vom Thema
-/// mitbekommt.
-///
-public sealed class StringEqualsConverter : IValueConverter
-{
- public static readonly StringEqualsConverter Instance = new();
-
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is string text && parameter is string expected
- && text.Equals(expected, StringComparison.OrdinalIgnoreCase);
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException("Nur zur Anzeige.");
-}
diff --git a/src/ClawdDotNet.Desktop/LauncherRun.cs b/src/ClawdDotNet.Desktop/LauncherRun.cs
new file mode 100644
index 0000000..3b35a4b
--- /dev/null
+++ b/src/ClawdDotNet.Desktop/LauncherRun.cs
@@ -0,0 +1,180 @@
+using System.Diagnostics;
+using System.Net.Sockets;
+using System.Text.Json;
+using ClawdDotNet.Core.Storage;
+
+namespace ClawdDotNet.Desktop;
+
+///
+/// #764: ClawdDotNet started without a switch — by a double click, a start menu entry,
+/// a desktop file.
+///
+/// Until #764 that opened the Avalonia window. The windows are gone; the web interface is
+/// the only one. What a person who starts the program wants is therefore: the instance manager
+/// running, and its web interface in front of them. Until the shell with its own window exists
+/// (#764, stage 2) the system browser shows it.
+///
+/// Never a second manager. If the manager answers, it is left alone. If it runs as
+/// a systemd service on this machine, the service is started — starting the program next to it
+/// would put two managers on the same instances. Only otherwise is the manager program started
+/// directly.
+///
+internal static class LauncherRun
+{
+ private const int ExitOk = 0;
+ private const int ExitNoManager = 6;
+
+ private const int DefaultPort = 7000;
+ private const string ServiceUnit = "clawddotnet-manager.service";
+ private static readonly TimeSpan StartWait = TimeSpan.FromSeconds(30);
+
+ public static async Task RunAsync()
+ {
+ var port = ManagerPort();
+
+ if (!Answers(port))
+ {
+ var problem = StartManager();
+ if (problem is not null)
+ {
+ await Console.Error.WriteLineAsync(problem);
+ return ExitNoManager;
+ }
+
+ var until = DateTime.UtcNow + StartWait;
+ while (!Answers(port) && DateTime.UtcNow < until)
+ await Task.Delay(500);
+
+ if (!Answers(port))
+ {
+ await Console.Error.WriteLineAsync(
+ $"The instance manager did not answer on port {port} within {StartWait.TotalSeconds:0} s. "
+ + "Its log is under Logs/Manager in the data directory.");
+ return ExitNoManager;
+ }
+ }
+
+ var url = $"http://localhost:{port}/";
+ Console.WriteLine(url);
+ SystemShell.OpenUrl(url);
+ return ExitOk;
+ }
+
+ /// Why the manager could not be started, or null.
+ private static string? StartManager()
+ {
+ if (OperatingSystem.IsLinux() && Systemctl("cat", ServiceUnit) == 0)
+ {
+ return Systemctl("start", ServiceUnit) == 0
+ ? null
+ : $"systemctl --user start {ServiceUnit} failed; see journalctl --user -u {ServiceUnit}.";
+ }
+
+ if (FindManagerExecutable() is not { } manager)
+ {
+ return "The instance manager (ClawdDotNet.Instanzmanager) was not found next to this program.";
+ }
+
+ try
+ {
+ // On Windows with its own console window: that is where the manager shows the access
+ // token on first start. Elsewhere a plain child that outlives us.
+ Process.Start(new ProcessStartInfo(manager)
+ {
+ UseShellExecute = OperatingSystem.IsWindows(),
+ WorkingDirectory = Path.GetDirectoryName(manager)!
+ });
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return $"The instance manager could not be started: {ex.Message}";
+ }
+ }
+
+ private static bool Answers(int port)
+ {
+ try
+ {
+ using var client = new TcpClient();
+ return client.ConnectAsync("127.0.0.1", port).Wait(TimeSpan.FromSeconds(1)) && client.Connected;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ private static int Systemctl(string verb, string unit)
+ {
+ try
+ {
+ var start = new ProcessStartInfo("systemctl") { RedirectStandardOutput = true, RedirectStandardError = true };
+ start.ArgumentList.Add("--user");
+ start.ArgumentList.Add(verb);
+ start.ArgumentList.Add(unit);
+ using var p = Process.Start(start)!;
+ p.StandardOutput.ReadToEnd();
+ p.StandardError.ReadToEnd();
+ p.WaitForExit(30_000);
+ return p.HasExited ? p.ExitCode : -1;
+ }
+ catch (Exception)
+ {
+ return -1; // no systemd here
+ }
+ }
+
+ ///
+ /// Next to this program in an installation; the build folders in a development checkout.
+ ///
+ /// Built from segments, not as a string: a path with backslashes is one file name on
+ /// Linux, and the search never found anything there (moved here from App.axaml.cs).
+ ///
+ private static string? FindManagerExecutable()
+ {
+ var name = OperatingSystem.IsWindows() ? "ClawdDotNet.Instanzmanager.exe" : "ClawdDotNet.Instanzmanager";
+
+ var nextTo = Path.Combine(AppContext.BaseDirectory, name);
+ if (File.Exists(nextTo)) return nextTo;
+
+ string[][] developmentPaths =
+ [
+ ["..", "..", "..", "..", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"],
+ ["..", "..", "..", "..", "..", "src", "ClawdDotNet.ManagerApp", "bin", "Debug", "net10.0"]
+ ];
+
+ foreach (var segments in developmentPaths)
+ {
+ var path = Path.GetFullPath(Path.Combine([AppContext.BaseDirectory, .. segments, name]));
+ if (File.Exists(path)) return path;
+ }
+
+ return null;
+ }
+
+ ///
+ /// The port from manager.json; the default on any doubt. Read as one field, not as
+ /// the manager's settings type, which this program does not know.
+ ///
+ private static int ManagerPort()
+ {
+ try
+ {
+ var file = Path.Combine(AppPaths.ConfigDirectory, "manager.json");
+ if (!File.Exists(file)) return DefaultPort;
+
+ using var doc = JsonDocument.Parse(File.ReadAllText(file));
+ return doc.RootElement.TryGetProperty("webUiPort", out var port)
+ && port.ValueKind == JsonValueKind.Number
+ && port.TryGetInt32(out var value)
+ && value is > 0 and < 65536
+ ? value
+ : DefaultPort;
+ }
+ catch (Exception)
+ {
+ return DefaultPort;
+ }
+ }
+}
diff --git a/src/ClawdDotNet.Desktop/Program.cs b/src/ClawdDotNet.Desktop/Program.cs
index b3fddfe..c0ba1e8 100644
--- a/src/ClawdDotNet.Desktop/Program.cs
+++ b/src/ClawdDotNet.Desktop/Program.cs
@@ -1,25 +1,21 @@
-using Avalonia;
-
namespace ClawdDotNet.Desktop;
internal static class Program
{
///
- /// Einstiegspunkt der Oberfläche.
+ /// Einstiegspunkt.
///
- /// Bewusst schlank: Hier wird nur Avalonia hochgefahren. Alles Fachliche —
- /// Einstellungen, Instanzauswahl, Engine, Scanner, Watchdog — baut
- /// über die Anwendungsschicht auf, damit derselbe Aufbau später
- /// auch ohne Fenster laufen kann.
+ /// #764: Bis hierher fuhr ohne Schalter die Avalonia-Oberfläche hoch. Die Fenster
+ /// sind entfernt — die Weboberfläche ist die einzige Oberfläche. Die Programmdatei bleibt,
+ /// was sie für den Betrieb immer war: der Instanzprozess (--headless --managed
+ /// --instance …, so startet der Manager jede Instanz), die MCP-Brücke und der Starter
+ /// in der Sandbox. Ohne Schalter startet sie den Instanzmanager, falls er nicht läuft, und
+ /// öffnet die Weboberfläche ().
///
- /// Kein [STAThread] mehr: Das war eine COM-Anforderung von WinForms. Avalonia
- /// braucht es nicht, und unter Linux hätte es ohnehin keine Bedeutung.
+ /// Kein Fenstersystem wird angefasst — auf einem Server ohne Bildschirmsitzung gäbe
+ /// es keines (#528, Stufe 2). Das galt bisher für --headless und gilt jetzt für
+ /// alles.
///
- ///
- /// Die Verzweigung steht vor und nicht darin: Mit
- /// --headless soll gar kein Fenstersystem angefasst werden — auf einem Server
- /// ohne Bildschirmsitzung gäbe es keines (#528, Stufe 2).
- ///
public static int Main(string[] args)
=> CommandLine.IsSandboxLaunch(args)
? Core.Sandbox.SandboxLauncher.RunAsync(args).GetAwaiter().GetResult()
@@ -27,19 +23,5 @@ internal static class Program
? McpBridgeRun.RunAsync(args).GetAwaiter().GetResult()
: CommandLine.IsHeadless(args)
? HeadlessRun.RunAsync(args).GetAwaiter().GetResult()
- : BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
-
- ///
- /// Auch vom Vorschau-Werkzeug des Editors aufgerufen — deshalb öffentlich und
- /// getrennt von .
- ///
- public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure()
- .UsePlatformDetect()
- // Kein WithInterFont mehr: Die Oberfläche bringt seit dem Umbau auf den
- // Entwurf Barlow und Barlow Condensed als eigene Ressourcen mit
- // (Assets/Fonts, referenziert in Styles/Theme.axaml). Der Grund von damals
- // gilt unverändert — ein schlankes Linux-Abbild hat oft gar keine
- // Systemschrift —, nur liegt die mitgelieferte Schrift jetzt woanders.
- .LogToTrace();
+ : LauncherRun.RunAsync().GetAwaiter().GetResult();
}
diff --git a/src/ClawdDotNet.Desktop/Services/AvaloniaLicensePrompt.cs b/src/ClawdDotNet.Desktop/Services/AvaloniaLicensePrompt.cs
deleted file mode 100644
index 573fcd7..0000000
--- a/src/ClawdDotNet.Desktop/Services/AvaloniaLicensePrompt.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Threading;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.Desktop.ViewModels;
-using ClawdDotNet.Desktop.Views;
-using ClawdDotNet.Licensing;
-
-namespace ClawdDotNet.Desktop.Services;
-
-///
-/// Lizenzabfrage über Fenster.
-///
-/// Der Aufbau läuft auf einem Hintergrundfaden — Avalonia besteht aber darauf, dass
-/// Fenster auf dem Oberflächenfaden entstehen. Deshalb geht hier alles über
-/// . Das ist der Grund, warum
-/// durchgehend asynchron ist: Die WinForms-Fassung konnte
-/// ShowDialog einfach blockierend aufrufen, weil sie ohnehin auf dem
-/// Oberflächenfaden lief.
-///
-public sealed class AvaloniaLicensePrompt : ILicensePrompt
-{
- public async Task RequestKeyAsync(string hardwareId, string? problem, string? currentKey)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var viewModel = new LicenseViewModel(hardwareId, problem, currentKey);
- var window = new LicenseWindow { DataContext = viewModel };
-
- window.Show();
-
- var key = await viewModel.Result;
- window.Close();
-
- return key;
- });
-
- public Task ShowInfoAsync(string title, string message)
- => ShowMessageAsync(title, message);
-
- public Task ShowErrorAsync(string title, string message)
- => ShowMessageAsync(title, message);
-
- ///
- /// Ein Meldungsfenster.
- ///
- /// Avalonia bringt kein MessageBox mit — bewusst, weil es auf allen
- /// Plattformen anders aussähe. Ein schlichtes Fenster ist hier ausreichend und
- /// erspart uns eine weitere Abhängigkeit für drei Aufrufstellen.
- ///
- private static async Task ShowMessageAsync(string title, string message)
- => await Dispatcher.UIThread.InvokeAsync(async () =>
- {
- var completion = new TaskCompletionSource();
-
- var button = new Button
- {
- Content = "OK",
- HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
- IsDefault = true
- };
-
- var window = new Window
- {
- Title = title,
- Width = 460,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Content = new StackPanel
- {
- Margin = new Avalonia.Thickness(20),
- Spacing = 16,
- Children =
- {
- new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
- button
- }
- }
- };
-
- button.Click += (_, _) => window.Close();
- window.Closed += (_, _) => completion.TrySetResult();
-
- window.Show();
- await completion.Task;
- });
-}
diff --git a/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs b/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs
deleted file mode 100644
index c64bb3c..0000000
--- a/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs
+++ /dev/null
@@ -1,199 +0,0 @@
-using System.ComponentModel;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Text.Json;
-
-namespace ClawdDotNet.Desktop.Services;
-
-public sealed record LanguageOption(string Code, string Name)
-{
- public override string ToString() => Name;
-}
-
-///
-/// Manages application localization for Avalonia UI.
-/// Supports JSON language packs with fallback to English, dynamic language switching,
-/// and live UI re-binding via indexer property notification.
-///
-public sealed class LocalizationManager : INotifyPropertyChanged
-{
- public static LocalizationManager Instance { get; } = new();
-
- public const string DefaultLanguage = "en";
- public const string FallbackLanguage = "en";
-
- private readonly Dictionary _fallbackStrings = new(StringComparer.OrdinalIgnoreCase);
- private Dictionary _activeStrings = new(StringComparer.OrdinalIgnoreCase);
- private string _currentLanguage = DefaultLanguage;
-
- public IReadOnlyList AvailableLanguages { get; } =
- [
- new("en", "English"),
- new("de", "Deutsch")
- ];
-
- public event PropertyChangedEventHandler? PropertyChanged;
- public event Action? LanguageChanged;
-
- public string CurrentLanguage
- {
- get => _currentLanguage;
- private set
- {
- if (_currentLanguage != value)
- {
- _currentLanguage = value;
- OnPropertyChanged();
- }
- }
- }
-
- [IndexerName("Item")]
- public string this[string key] => Get(key);
-
- private LocalizationManager()
- {
- LoadFallbackPack();
- SetLanguage(DefaultLanguage);
- }
-
- private void LoadFallbackPack()
- {
- _fallbackStrings.Clear();
- var json = LoadLanguageJson(FallbackLanguage);
- if (!string.IsNullOrWhiteSpace(json))
- {
- ParseJsonIntoDictionary(json, _fallbackStrings);
- }
- }
-
- public void SetLanguage(string? languageCode)
- {
- var target = string.IsNullOrWhiteSpace(languageCode) ? DefaultLanguage : languageCode.Trim().ToLowerInvariant();
-
- if (target == FallbackLanguage)
- {
- _activeStrings = _fallbackStrings;
- }
- else
- {
- var pack = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var json = LoadLanguageJson(target);
- if (!string.IsNullOrWhiteSpace(json))
- {
- ParseJsonIntoDictionary(json, pack);
- }
- _activeStrings = pack;
- }
-
- CurrentLanguage = target;
-
- // Notify indexer bindings so Avalonia refreshes all localized strings on screen
- OnPropertyChanged("Item");
- OnPropertyChanged("Item[]");
- LanguageChanged?.Invoke();
- }
-
- public string Get(string key, params object[] args)
- {
- if (string.IsNullOrEmpty(key)) return "";
-
- string? template = null;
- if (!_activeStrings.TryGetValue(key, out template))
- {
- _fallbackStrings.TryGetValue(key, out template);
- }
-
- template ??= key;
-
- if (args.Length > 0)
- {
- try
- {
- return string.Format(template, args);
- }
- catch
- {
- return template;
- }
- }
-
- return template;
- }
-
- private static string? LoadLanguageJson(string langCode)
- {
- // 1. Try file on disk in AppContext.BaseDirectory/i18n/{langCode}.json
- var localPath = Path.Combine(AppContext.BaseDirectory, "i18n", $"{langCode}.json");
- if (File.Exists(localPath))
- {
- try
- {
- return File.ReadAllText(localPath);
- }
- catch
- {
- // Fall through to embedded resource
- }
- }
-
- // 2. Try embedded resource in ClawdDotNet assembly
- var assembly = Assembly.GetExecutingAssembly();
- var resourceName = assembly.GetManifestResourceNames()
- .FirstOrDefault(r => r.EndsWith($"i18n.{langCode}.json", StringComparison.OrdinalIgnoreCase));
-
- if (resourceName is not null)
- {
- using var stream = assembly.GetManifestResourceStream(resourceName);
- if (stream is not null)
- {
- using var reader = new StreamReader(stream);
- return reader.ReadToEnd();
- }
- }
-
- return null;
- }
-
- private static void ParseJsonIntoDictionary(string json, Dictionary dict)
- {
- try
- {
- using var doc = JsonDocument.Parse(json);
- FlattenElement("", doc.RootElement, dict);
- }
- catch
- {
- // Invalid JSON ignored
- }
- }
-
- private static void FlattenElement(string prefix, JsonElement element, Dictionary dict)
- {
- switch (element.ValueKind)
- {
- case JsonValueKind.Object:
- foreach (var prop in element.EnumerateObject())
- {
- var childPrefix = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
- FlattenElement(childPrefix, prop.Value, dict);
- }
- break;
-
- case JsonValueKind.String:
- dict[prefix] = element.GetString() ?? "";
- break;
-
- case JsonValueKind.Number:
- case JsonValueKind.True:
- case JsonValueKind.False:
- dict[prefix] = element.ToString();
- break;
- }
- }
-
- private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs b/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs
deleted file mode 100644
index 2e89a57..0000000
--- a/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Avalonia.Data;
-using Avalonia.Markup.Xaml;
-
-namespace ClawdDotNet.Desktop.Services;
-
-///
-/// Markup extension for Avalonia XAML:
-/// Usage: Text="{services:Localize nav.chat}"
-/// Updates automatically when language changes.
-///
-public sealed class LocalizeExtension : MarkupExtension
-{
- public string Key { get; set; } = "";
-
- public LocalizeExtension() { }
-
- public LocalizeExtension(string key)
- {
- Key = key;
- }
-
- public override object ProvideValue(IServiceProvider serviceProvider)
- {
- return new Binding($"[{Key}]")
- {
- Source = LocalizationManager.Instance,
- Mode = BindingMode.OneWay
- };
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Services/ThemeManager.cs b/src/ClawdDotNet.Desktop/Services/ThemeManager.cs
deleted file mode 100644
index 9ed475b..0000000
--- a/src/ClawdDotNet.Desktop/Services/ThemeManager.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Avalonia;
-using Avalonia.Styling;
-
-namespace ClawdDotNet.Desktop.Services;
-
-///
-/// Hell/Dunkel umschalten — an genau einer Stelle.
-///
-/// Die Farbwerte selbst stehen in Styles/Theme.axaml als
-/// ThemeDictionaries. Hier wird nur umgelegt, welches der beiden Wörterbücher gilt;
-/// jede Bindung mit DynamicResource zieht dann von allein nach. Deshalb gibt es
-/// hier auch keine Liste von Steuerelementen, die nachgefärbt werden müssten — die wäre
-/// beim nächsten neuen Bereich schon wieder unvollständig.
-///
-public static class ThemeManager
-{
- /// Name in den Einstellungen, wenn dunkel gewählt ist.
- public const string DarkName = "Dark";
-
- /// Übersetzt den gespeicherten Namen; alles außer „Dark" gilt als hell.
- public static bool IsDarkName(string? name)
- => string.Equals(name, DarkName, StringComparison.OrdinalIgnoreCase);
-
- public static string ToName(bool isDark) => isDark ? DarkName : "Light";
-
- ///
- /// Legt die Variante an der Anwendung um. Ohne laufende Anwendung — etwa im
- /// Entwurfsmodus des Editors — passiert nichts.
- ///
- public static void Apply(bool isDark)
- {
- if (Application.Current is not { } app) return;
- app.RequestedThemeVariant = isDark ? ThemeVariant.Dark : ThemeVariant.Light;
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Styles/Icons.axaml b/src/ClawdDotNet.Desktop/Styles/Icons.axaml
deleted file mode 100644
index 9848df3..0000000
--- a/src/ClawdDotNet.Desktop/Styles/Icons.axaml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
- M12 9c-1.5-2.5-4-3-5.5-1.5S5 11 7 12c-1 1-1.5 2-1.5 3.5S7 18 8.5 17c1-1 1.5-2 1.5-3 M12 9c1.5-2.5 4-3 5.5-1.5S19 11 17 12c1 1 1.5 2 1.5 3.5S15 18 13.5 17c-1-1-1.5-2-1.5-3 M12 9v3
-
-
- M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z
- M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M13 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0 M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75
- M3 17l2 2 4-4 M3 7l2 2 4-4 M13 6h8 M13 12h8 M13 18h8
- M3 3v18h18 M7 15v4 M11 11v8 M15 7v12 M19 12v7
- M8 12h.01 M12 12h.01 M16 12h.01 M21 12c0 4.4-4 8-9 8-1.3 0-2.6-.2-3.7-.7L3 20l1-4.2C3.4 14.6 3 13.3 3 12c0-4.4 4-8 9-8s9 3.6 9 8Z
- M3 4h18a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9 M10 13h4
- M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M9 13h6 M9 17h6
- M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9c.13-.32.19-.66.18-1a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06c.51.5 1.24.66 1.82.33H9c.32-.13.66-.19 1-.18A1.65 1.65 0 0 0 11.51 3H12a2 2 0 0 1 4 0v.09c0 .34.06.68.18 1a1.65 1.65 0 0 0 1.82.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06c-.51.5-.67 1.24-.33 1.82V9c.13.32.19.66.18 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z
- M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0 M12 16v-4 M12 8h.01
-
-
- M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0 M12 2v2 M12 20v2 M4.9 4.9l1.4 1.4 M17.7 17.7l1.4 1.4 M2 12h2 M20 12h2 M4.9 19.1l1.4-1.4 M17.7 6.3l1.4-1.4
- M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z
- M15 18l-6-6 6-6
- M9 18l6-6-6-6
- M6 9l6 6 6-6
-
-
- M12 9v4 M12 17h.01 M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z
- M18 6L6 18 M6 6l12 12
-
-
- M12 5v14 M5 12h14
- M5 12h14
- M8 5v14l11-7z
- M6 6h12v12H6z
- M12 20h9 M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z
- M3 6h18 M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6
- M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2Z
- M23 4v6h-6 M1 20v-6h6 M3.51 9a9 9 0 0 1 14.85-3.36L23 10 M20.49 15a9 9 0 0 1-14.85 3.36L1 14
- M18.36 6.64a9 9 0 1 1-12.73 0 M12 2v10
- M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z M17 21v-8H7v8 M7 3v5h8
-
-
diff --git a/src/ClawdDotNet.Desktop/Styles/Shell.axaml b/src/ClawdDotNet.Desktop/Styles/Shell.axaml
deleted file mode 100644
index 17e7013..0000000
--- a/src/ClawdDotNet.Desktop/Styles/Shell.axaml
+++ /dev/null
@@ -1,862 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Styles/Theme.axaml b/src/ClawdDotNet.Desktop/Styles/Theme.axaml
deleted file mode 100644
index a809100..0000000
--- a/src/ClawdDotNet.Desktop/Styles/Theme.axaml
+++ /dev/null
@@ -1,174 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #f2f2f3
- #e9e9ea
- #1d1f20
- #5980a6
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #1a1c1e
- #232527
- #eef0f1
- #7ea3c6
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- avares://ClawdDotNet/Assets/Fonts#Barlow Condensed
- avares://ClawdDotNet/Assets/Fonts#Barlow
- Cascadia Mono,Consolas,Menlo,DejaVu Sans Mono,monospace
-
-
diff --git a/src/ClawdDotNet.Desktop/ViewLocator.cs b/src/ClawdDotNet.Desktop/ViewLocator.cs
deleted file mode 100644
index 9693908..0000000
--- a/src/ClawdDotNet.Desktop/ViewLocator.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop;
-
-///
-/// Findet zu einem Ansichtsmodell die passende Ansicht über die Namenskonvention
-/// …ViewModels.FooViewModel → …Views.FooView.
-///
-/// Das erspart es, jede Zuordnung von Hand als DataTemplate einzutragen — bei
-/// den sieben Bereichen und ihren Unteransichten wären das schnell dreißig Einträge,
-/// die man beim Umbenennen übersieht.
-///
-/// Passt nichts, erscheint der gesuchte Typname im Fenster statt einer leeren Fläche.
-/// Beim schrittweisen Umbau ist eine sichtbare Fehlstelle mehr wert als ein stiller
-/// weißer Bereich.
-///
-public sealed class ViewLocator : IDataTemplate
-{
- public Control Build(object? data)
- {
- if (data is null)
- return new TextBlock { Text = "(kein Inhalt)" };
-
- var viewModelName = data.GetType().FullName!;
- var viewName = viewModelName
- .Replace(".ViewModels.", ".Views.", StringComparison.Ordinal)
- .Replace("ViewModel", "View", StringComparison.Ordinal);
-
- var type = Type.GetType(viewName);
-
- if (type is null)
- return new TextBlock { Text = $"Ansicht nicht gefunden: {viewName}" };
-
- return (Control)Activator.CreateInstance(type)!;
- }
-
- public bool Match(object? data) => data is ViewModelBase;
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs
deleted file mode 100644
index c10ef19..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record AddAgentResultData(
- string AgentId,
- string DisplayName,
- string Model,
- string Description
-);
-
-public sealed partial class AddAgentViewModel : ViewModelBase
-{
- [ObservableProperty]
- private string _agentId = "";
-
- [ObservableProperty]
- private string _displayName = "";
-
- [ObservableProperty]
- private string _selectedModel = "anthropic/claude-3-5-sonnet";
-
- [ObservableProperty]
- private string _description = "";
-
- [ObservableProperty]
- private string _statusMessage = "";
-
- public List AvailableModels { get; } =
- [
- "anthropic/claude-3-5-sonnet",
- "anthropic/claude-3-opus",
- "openai/gpt-4o",
- "openai/gpt-4o-mini",
- "google/gemini-2.5-flash",
- "google/gemini-2.5-pro",
- "deepseek/deepseek-r1"
- ];
-
- public AddAgentResultData? Result { get; private set; }
-
- public event Action? CloseRequested;
-
- [RelayCommand]
- private void Confirm()
- {
- if (string.IsNullOrWhiteSpace(AgentId))
- {
- StatusMessage = "Please enter an Agent ID.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(DisplayName))
- {
- StatusMessage = "Please enter a display name.";
- return;
- }
-
- Result = new AddAgentResultData(
- AgentId.Trim(),
- DisplayName.Trim(),
- SelectedModel,
- Description.Trim()
- );
-
- CloseRequested?.Invoke();
- }
-
- [RelayCommand]
- private void Cancel()
- {
- Result = null;
- CloseRequested?.Invoke();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs
deleted file mode 100644
index f070f11..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using ClawdDotNet.Core.Config;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record AddJobResultData(
- string AgentId,
- bool IsToolJob,
- string ToolName,
- string JobTypeId,
- string CronExpression,
- string TaskMessage,
- bool RunOnStart
-);
-
-public sealed partial class AddJobViewModel : ViewModelBase
-{
- public List AvailableAgents { get; }
-
- [ObservableProperty]
- private AgentConfig? _selectedAgent;
-
- [ObservableProperty]
- private bool _isToolJob;
-
- [ObservableProperty]
- private bool _isAgentWakeup = true;
-
- [ObservableProperty]
- private string _selectedToolName = "FileRW";
-
- [ObservableProperty]
- private string _jobTypeId = "PollJob";
-
- [ObservableProperty]
- private string _cronExpression = "0 * * * *";
-
- [ObservableProperty]
- private string _taskMessage = "Execute your assigned tasks.";
-
- [ObservableProperty]
- private bool _runOnStart;
-
- [ObservableProperty]
- private string _statusMessage = "";
-
- public List AvailableTools { get; } =
- [
- "FileRW", "Telegram", "Mail", "Database", "FTP",
- "DirectAPI", "WebFetch", "WebMonitor", "AgentComm",
- "SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard"
- ];
-
- public AddJobResultData? Result { get; private set; }
-
- public event Action? CloseRequested;
-
- public AddJobViewModel() : this([]) { }
-
- public AddJobViewModel(List agents)
- {
- AvailableAgents = agents;
- SelectedAgent = AvailableAgents.FirstOrDefault();
- }
-
- partial void OnIsToolJobChanged(bool value)
- {
- IsAgentWakeup = !value;
- }
-
- partial void OnIsAgentWakeupChanged(bool value)
- {
- IsToolJob = !value;
- }
-
- [RelayCommand]
- private void Confirm()
- {
- if (SelectedAgent is null)
- {
- StatusMessage = "Please select an agent.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(CronExpression))
- {
- StatusMessage = "Please enter a cron expression.";
- return;
- }
-
- Result = new AddJobResultData(
- SelectedAgent.AgentId,
- IsToolJob,
- SelectedToolName,
- JobTypeId,
- CronExpression.Trim(),
- TaskMessage.Trim(),
- RunOnStart
- );
-
- CloseRequested?.Invoke();
- }
-
- [RelayCommand]
- private void Cancel()
- {
- Result = null;
- CloseRequested?.Invoke();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs
deleted file mode 100644
index 6b2f8fe..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record AddServiceResultData(
- string ServiceName,
- string ServiceType,
- int ServicePort,
- string ServiceDescription
-);
-
-public sealed partial class AddServiceViewModel : ViewModelBase
-{
- [ObservableProperty]
- private string _serviceName = "";
-
- [ObservableProperty]
- private string _serviceType = "HTTP";
-
- [ObservableProperty]
- private int _servicePort = 8080;
-
- [ObservableProperty]
- private string _serviceDescription = "";
-
- [ObservableProperty]
- private string _statusMessage = "";
-
- public List AvailableServiceTypes { get; } = ["HTTP", "TCP", "gRPC", "WebSocket", "Custom"];
-
- public AddServiceResultData? Result { get; private set; }
-
- public event Action? CloseRequested;
-
- [RelayCommand]
- private void Confirm()
- {
- if (string.IsNullOrWhiteSpace(ServiceName))
- {
- StatusMessage = "Please enter a service name.";
- return;
- }
-
- if (ServicePort <= 0 || ServicePort > 65535)
- {
- StatusMessage = "Please enter a valid port between 1 and 65535.";
- return;
- }
-
- Result = new AddServiceResultData(
- ServiceName.Trim(),
- ServiceType,
- ServicePort,
- ServiceDescription.Trim()
- );
-
- CloseRequested?.Invoke();
- }
-
- [RelayCommand]
- private void Cancel()
- {
- Result = null;
- CloseRequested?.Invoke();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs
deleted file mode 100644
index 35b319a..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using System.Collections.ObjectModel;
-using ClawdDotNet.App;
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-/// Eine Nachricht zwischen zwei Agenten.
-public sealed class AgentMessageItem
-{
- public required string SenderName { get; init; }
- public required string Text { get; init; }
- public required DateTime Time { get; init; }
-
- /// Links steht Agent A, rechts Agent B — wie im Chat mit dem Benutzer.
- public required bool IsFromAgentA { get; init; }
-}
-
-/// Ein Agent in den beiden Auswahlfeldern.
-public sealed record AgentChoice(string AgentId, string DisplayName)
-{
- public override string ToString() => DisplayName;
-}
-
-///
-/// Verlauf der Nachrichten zwischen zwei Agenten.
-///
-/// Ohne Datenbestand. Agenten können sich heute schon Nachrichten
-/// schicken — AgentEngine.SendMessageAsync stellt zu, aber nichts schreibt den
-/// Wortlaut mit. Der Speicher AgentMessages aus
-/// docs/Agentenkommunikation-Konzept.md ist noch nicht gebaut.
-///
-/// Die Seite steht trotzdem vollständig da: Auswahl, Filterung und Darstellung sind
-/// fertig, und wenn der Speicher kommt, ist hier genau eine Abfrage zu ergänzen —
-/// . Bis dahin erklärt die Seite ihren Leerzustand, statt als
-/// leerer Kasten dazustehen.
-///
-public sealed partial class AgentChatsPageViewModel : PageViewModel
-{
- public ObservableCollection Agents { get; } = [];
- public ObservableCollection Messages { get; } = [];
-
- [ObservableProperty]
- private AgentChoice? _agentA;
-
- [ObservableProperty]
- private AgentChoice? _agentB;
-
- [ObservableProperty]
- private bool _isEmpty = true;
-
- [ObservableProperty]
- private string _emptyText = "";
-
- public AgentChatsPageViewModel() : this(null) { }
-
- public AgentChatsPageViewModel(AppHost? host) : base("nav.agentChats")
- {
- if (host is null)
- {
- Agents.Add(new AgentChoice("dev-assistant", "Dev Assistant"));
- Agents.Add(new AgentChoice("social-media", "Social Media Manager"));
- }
- else
- {
- foreach (var agent in host.Instance.Agents)
- Agents.Add(new AgentChoice(agent.AgentId, agent.DisplayName));
- }
-
- AgentA = Agents.FirstOrDefault();
- AgentB = Agents.Skip(1).FirstOrDefault() ?? AgentA;
-
- LoadMessages();
- }
-
- partial void OnAgentAChanged(AgentChoice? value) => LoadMessages();
- partial void OnAgentBChanged(AgentChoice? value) => LoadMessages();
-
- ///
- /// Hier kommt die Abfrage hin, sobald der AgentMessages-Speicher steht: alle
- /// Nachrichten zwischen und in beiden
- /// Richtungen, nach Zeit sortiert.
- ///
- private void LoadMessages()
- {
- Messages.Clear();
-
- EmptyText = Agents.Count < 2
- ? LocalizationManager.Instance["desktop.agentChats.needTwo"]
- : LocalizationManager.Instance["desktop.agentChats.empty"];
-
- IsEmpty = Messages.Count == 0;
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs
deleted file mode 100644
index ebbde54..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs
+++ /dev/null
@@ -1,531 +0,0 @@
-using ClawdDotNet.Core.Tasks;
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.Core.Config;
-using ClawdDotNet.Core.Storage;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.Extensions.Logging;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed partial class ToolItemViewModel : ViewModelBase
-{
- public string Name { get; }
-
- ///
- /// Kurzbeschreibung aus der Werkzeug-Registrierung. Leer, wenn das Werkzeug in dieser
- /// Instanz gar nicht geladen ist — die Liste der bekannten Namen ist länger als die
- /// der tatsächlich vorhandenen Werkzeuge.
- ///
- public string Description { get; }
-
- [ObservableProperty]
- private bool _isEnabled;
-
- public ToolItemViewModel(string name, bool isEnabled, string description = "")
- {
- Name = name;
- Description = description;
- _isEnabled = isEnabled;
- }
-}
-
-public sealed partial class AgentItemViewModel : ViewModelBase
-{
- public AgentConfig Config { get; }
-
- public string AgentId => Config.AgentId;
-
- [ObservableProperty]
- private string _displayName;
-
- [ObservableProperty]
- private string _model;
-
- [ObservableProperty]
- private string _systemPrompt;
-
- [ObservableProperty]
- private string _identity;
-
- [ObservableProperty]
- private string _soul;
-
- [ObservableProperty]
- private string _description;
-
- [ObservableProperty]
- private string _promptCaching;
-
- [ObservableProperty]
- private int _maxToolResultChars;
-
- [ObservableProperty]
- private int _maxSteps;
-
- [ObservableProperty]
- private int _maxContextTokens;
-
- [ObservableProperty]
- private int _timeoutSeconds;
-
- [ObservableProperty]
- private decimal _dailyCostUsd;
-
- [ObservableProperty]
- private long _dailyTokens;
-
- /// #716: What happens to a recurring task's run when the budget is exhausted.
- [ObservableProperty]
- private string _whenExhausted = "expire";
-
- /// The values understands.
- public IReadOnlyList WhenExhaustedOptions { get; } = ["expire", "catchUpOnce"];
-
- public ObservableCollection AvailableTools { get; } = [];
-
- ///
- /// Die Namensliste steht in der Anwendungsschicht — die Weboberfläche braucht
- /// dieselbe, und zwei gepflegte Listen wären eine zu viel.
- ///
- public static IReadOnlyList KnownTools => AgentAdministration.KnownTools;
-
- public AgentItemViewModel(AgentConfig config, IReadOnlyDictionary? descriptions = null)
- {
- Config = config;
- _displayName = config.DisplayName;
- _model = config.Model;
- _systemPrompt = config.SystemPrompt;
- _identity = config.Identity;
- _soul = config.Soul;
- _description = config.Description;
- _promptCaching = config.PromptCaching;
- _maxToolResultChars = config.MaxToolResultChars;
- _maxSteps = config.LoopGuard.MaxSteps;
- _maxContextTokens = config.LoopGuard.MaxContextTokens;
- _timeoutSeconds = config.LoopGuard.TimeoutSeconds;
- _dailyCostUsd = config.Budget.DailyCostUsd;
- _dailyTokens = config.Budget.DailyTokens;
- _whenExhausted = config.Budget.MissPolicy == BudgetMissPolicy.CatchUpOnce ? "catchUpOnce" : "expire";
-
- foreach (var tool in KnownTools)
- {
- var isEnabled = config.Tools.ContainsKey(tool);
- var description = descriptions?.GetValueOrDefault(tool) ?? "";
-
- var item = new ToolItemViewModel(tool, isEnabled, description);
- item.PropertyChanged += (_, e) =>
- {
- if (e.PropertyName == nameof(ToolItemViewModel.IsEnabled))
- {
- if (item.IsEnabled && !Config.Tools.ContainsKey(tool))
- Config.Tools[tool] = new Dictionary();
- else if (!item.IsEnabled)
- Config.Tools.Remove(tool);
- }
- };
- AvailableTools.Add(item);
- }
- }
-
- public void ApplyChanges()
- {
- Config.DisplayName = DisplayName;
- Config.Model = Model;
- Config.SystemPrompt = SystemPrompt;
- Config.Identity = Identity;
- Config.Soul = Soul;
- Config.Description = Description;
- Config.PromptCaching = PromptCaching;
- Config.MaxToolResultChars = MaxToolResultChars;
- Config.LoopGuard.MaxSteps = MaxSteps;
- Config.LoopGuard.MaxContextTokens = MaxContextTokens;
- Config.LoopGuard.TimeoutSeconds = TimeoutSeconds;
- Config.Budget.DailyCostUsd = DailyCostUsd;
- Config.Budget.DailyTokens = DailyTokens;
- Config.Budget.WhenExhausted = WhenExhausted;
- }
-}
-
-/// Welches Feld der Texteditor gerade bearbeitet.
-public enum AgentTextField
-{
- Identity,
- Soul
-}
-
-///
-/// Ansichtsmodell für die Agenten-Verwaltungsseite.
-///
-/// Der Entwurf ordnet die Seite wieder wie die WinForms-Vorlage in vier Felder an:
-/// oben Agentenliste und Agenteneinstellungen, unten Werkzeugliste und
-/// Werkzeugeinstellungen. Für dieses Modell heißt das vor allem: Die Werkzeugeinstellungen
-/// sind kein eigenes Fenster mehr, sondern hängen als an der
-/// Auswahl in der Werkzeugliste.
-///
-public sealed partial class AgentsPageViewModel : PageViewModel
-{
- private readonly AppHost? _host;
- private readonly ILogger? _logger;
-
- public ObservableCollection Agents { get; } = [];
-
- [ObservableProperty]
- private AgentItemViewModel? _selectedAgent;
-
- ///
- /// Das ausgewählte Werkzeug des ausgewählten Agenten. Steuert, welche Felder unten
- /// rechts stehen.
- ///
- [ObservableProperty]
- private ToolItemViewModel? _selectedTool;
-
- ///
- /// Die Felder zum ausgewählten Werkzeug. Wird bei jedem Wechsel neu gebaut — das
- /// Ansichtsmodell trägt die Werte von einem guten Dutzend Werkzeuge, und eines davon
- /// mit den Werten eines anderen weiterzuverwenden ginge irgendwann schief.
- ///
- [ObservableProperty]
- private ToolSettingsViewModel? _toolSettings;
-
- [ObservableProperty]
- private string _statusText = "Bereit";
-
- [ObservableProperty]
- private bool _isRunning;
-
- public List KnownModels { get; } =
- [
- "anthropic/claude-3-5-sonnet",
- "anthropic/claude-3-opus",
- "openai/gpt-4o",
- "openai/gpt-4o-mini",
- "google/gemini-2.5-flash",
- "google/gemini-2.5-pro",
- "deepseek/deepseek-r1"
- ];
-
- public List CachingOptions { get; } = ["auto", "on", "off"];
-
- public event Func>? RequestAddAgentDialog;
-
- ///
- /// Öffnet den Editor für Identity bzw. Soul. Gibt der Aufrufer einen Text zurück,
- /// wurde gespeichert; null heißt Abbruch.
- ///
- public event Func>? RequestTextEditorDialog;
-
- public AgentsPageViewModel() : this(null) { }
-
- public AgentsPageViewModel(AppHost? host) : base("nav.agents")
- {
- _host = host;
-
- if (host is not null)
- {
- _logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Agents");
- ReloadAgents();
-
- // Seit es eine zweite Oberflaeche gibt, ist diese Liste nicht mehr die
- // einzige Quelle von Aenderungen: Ein im Browser angelegter Agent stuende
- // sonst in der Datei und nicht im Fenster. Das Ereignis kommt auf dem Faden
- // des Verursachers — deshalb ueber den Dispatcher.
- //
- // Kein Abmelden: Diese Seite lebt so lange wie die Anwendung.
- host.InstanceChanged += change =>
- {
- if (change.Kind == InstanceChangeKind.Agents)
- Dispatcher.UIThread.Post(ReloadAgents);
- };
- }
- else
- {
- StatusText = LocalizationManager.Instance["desktop.status.design"];
- }
-
- LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(AllowedToolsHeader));
- }
-
- ///
- /// Beschreibungen der tatsächlich geladenen Werkzeuge. Die Namensliste in
- /// ist fest verdrahtet und kennt auch
- /// Werkzeuge, die in dieser Instanz nicht registriert sind — für die bleibt die
- /// Beschreibung eben leer.
- ///
- private IReadOnlyDictionary ToolDescriptions()
- => _host?.AgentAdmin.ToolDescriptions() ?? new Dictionary();
-
- ///
- /// Baut die Liste neu auf und behaelt die Auswahl, sofern es sie noch gibt.
- ///
- /// Die Auswahl zu behalten ist kein Feinschliff: Der Aufbau laeuft jetzt auch,
- /// wenn die Weboberflaeche etwas geaendert hat. Waere die Auswahl danach jedesmal
- /// weg, spraenge dem Benutzer der Cursor aus der Zeile, waehrend er arbeitet.
- ///
- [RelayCommand]
- private void ReloadAgents()
- {
- if (_host is null)
- {
- Agents.Clear();
- return;
- }
-
- var previous = SelectedAgent?.AgentId;
-
- Agents.Clear();
-
- var descriptions = ToolDescriptions();
-
- foreach (var agent in _host.Instance.Agents)
- Agents.Add(new AgentItemViewModel(agent, descriptions));
-
- SelectedAgent = Agents.FirstOrDefault(a => a.AgentId == previous) ?? Agents.FirstOrDefault();
- }
-
- public string AllowedToolsHeader => SelectedAgent is not null
- ? LocalizationManager.Instance.Get("desktop.agents.allowedToolsFor", SelectedAgent.DisplayName)
- : "";
-
- partial void OnSelectedAgentChanged(AgentItemViewModel? value)
- {
- // Die Werkzeugliste unten gehört zum gewählten Agenten — bei einem Wechsel darf
- // dort nicht die Auswahl des vorigen stehen bleiben.
- SelectedTool = value?.AvailableTools.FirstOrDefault();
- OnPropertyChanged(nameof(AllowedToolsHeader));
- }
-
- partial void OnSelectedToolChanged(ToolItemViewModel? value)
- {
- if (SelectedAgent is null || value is null)
- {
- ToolSettings = null;
- return;
- }
-
- // Auch für ein noch nicht zugelassenes Werkzeug sollen die Felder sichtbar sein —
- // sonst müsste man erst ankreuzen, um zu sehen, was einzustellen wäre.
- if (!SelectedAgent.Config.Tools.TryGetValue(value.Name, out var config) || config is null)
- config = new Dictionary();
-
- ToolSettings = new ToolSettingsViewModel(value.Name, config);
- }
-
- [RelayCommand]
- private async Task AddAgentAsync()
- {
- if (_host is null || RequestAddAgentDialog is null) return;
-
- var vm = new AddAgentViewModel();
- var result = await RequestAddAgentDialog(vm);
- if (result is null) return;
-
- // Erst die offenen Änderungen der Liste sichern, dann anlegen: Der Aufruf
- // schreibt die Instanzdatei, und was hier noch nicht übernommen war, ginge sonst
- // verloren.
- ApplyPendingEdits();
-
- var outcome = _host.AgentAdmin.Add(
- new NewAgent(result.AgentId, result.DisplayName, result.Model, result.Description));
-
- StatusText = outcome.Message;
- if (!outcome.Ok) return;
-
- if (_host.AgentAdmin.Find(result.AgentId) is not { } newConfig) return;
-
- var item = new AgentItemViewModel(newConfig, ToolDescriptions());
- Agents.Add(item);
- SelectedAgent = item;
- }
-
- [RelayCommand]
- private void RemoveAgent()
- {
- if (_host is null || SelectedAgent is null) return;
-
- var removed = SelectedAgent;
- ApplyPendingEdits();
-
- var outcome = _host.AgentAdmin.Remove(removed.AgentId);
- StatusText = outcome.Message;
- if (!outcome.Ok) return;
-
- Agents.Remove(removed);
- SelectedAgent = Agents.FirstOrDefault();
- }
-
- ///
- /// Weckt den gewählten Agenten von Hand — im Entwurf die Schaltfläche „Jetzt ausf.".
- /// Nimmt dieselbe Nachricht wie der geplante Lauf, damit der Handlauf zeigt, was der
- /// geplante täte.
- ///
- [RelayCommand]
- private async Task RunAgentNowAsync()
- {
- if (_host is null || SelectedAgent is null) return;
-
- if (_host.Engine is null)
- {
- StatusText = LocalizationManager.Instance["desktop.chat.noApiKey"];
- return;
- }
-
- var config = SelectedAgent.Config;
-
- IsRunning = true;
- StatusText = $"Running '{config.DisplayName}'…";
-
- try
- {
- var outcome = await _host.Runs.RunAgentAsync(
- config.AgentId, config.Scheduler?.TaskMessage, CancellationToken.None);
-
- StatusText = outcome.Message;
-
- if (!outcome.Ok)
- _logger?.LogWarning("Agent execution failed: {Reason}", outcome.Message);
- }
- finally
- {
- IsRunning = false;
- }
- }
-
- [RelayCommand]
- private Task EditIdentityAsync() => EditTextFieldAsync(AgentTextField.Identity);
-
- [RelayCommand]
- private Task EditSoulAsync() => EditTextFieldAsync(AgentTextField.Soul);
-
- ///
- /// Identity und Soul sind lange Texte. Der Entwurf gibt ihnen ein eigenes kleines
- /// Fenster statt eines Feldes in der Spalte — in einem 60 Pixel hohen Kasten lässt
- /// sich eine Rollenbeschreibung nicht schreiben.
- ///
- private async Task EditTextFieldAsync(AgentTextField field)
- {
- if (SelectedAgent is null || RequestTextEditorDialog is null) return;
-
- var isIdentity = field == AgentTextField.Identity;
- var title = isIdentity
- ? $"Edit Identity — {SelectedAgent.DisplayName}"
- : $"Edit Soul — {SelectedAgent.DisplayName}";
-
- var current = isIdentity ? SelectedAgent.Identity : SelectedAgent.Soul;
-
- var edited = await RequestTextEditorDialog(title, current);
- if (edited is null) return;
-
- if (isIdentity) SelectedAgent.Identity = edited;
- else SelectedAgent.Soul = edited;
-
- SaveAgents();
- }
-
- /// Öffnet den Arbeitsordner des gewählten Agenten.
- [RelayCommand]
- private void OpenAgentFolder()
- {
- if (_host is null || SelectedAgent is null) return;
-
- var path = _host.Directories.GetAgentPath(_host.InstancePath, SelectedAgent.DisplayName);
-
- if (!Directory.Exists(path))
- {
- StatusText = $"Folder not found: {path}";
- return;
- }
-
- SystemShell.OpenFolder(path);
- }
-
- ///
- /// Übernimmt die Felder des gewählten Werkzeugs in die Agentenkonfiguration.
- ///
- /// Das Ankreuzfeld in der Liste und diese Schaltfläche sind zwei verschiedene
- /// Dinge: Ersteres lässt das Werkzeug zu, letztere schreibt seine Einstellungen. Wer
- /// nur ankreuzt, bekommt ein Werkzeug mit Vorgabewerten — das ist gewollt.
- ///
- [RelayCommand]
- private void SaveToolSettings()
- {
- if (_host is null || SelectedAgent is null || SelectedTool is null || ToolSettings is null) return;
-
- ToolSettings.ConfirmCommand.Execute(null);
-
- if (ToolSettings.Result is not { } config) return;
-
- // Einstellungen zu schreiben heißt, das Werkzeug zu wollen — das Ankreuzfeld
- // zieht nach, damit Liste und Konfiguration nicht auseinanderlaufen.
- SelectedTool.IsEnabled = true;
-
- ApplyPendingEdits();
- StatusText = _host.AgentAdmin
- .SetToolSettings(SelectedAgent.AgentId, SelectedTool.Name, config).Message;
- }
-
- [RelayCommand]
- private void SaveAgents()
- {
- if (_host is null) return;
-
- ApplyPendingEdits();
- _host.AgentAdmin.Save();
-
- _logger?.LogInformation("Agents configuration saved.");
- StatusText = LocalizationManager.Instance["desktop.agents.saved"];
- }
-
- ///
- /// Überträgt, was in den Feldern der Seite steht, in die Konfigurationsobjekte.
- ///
- /// Die Ansichtsmodelle halten eigene Eigenschaften und schreiben sie erst bei
- /// ApplyChanges zurück. Jeder Aufruf, der gleich die Instanzdatei schreibt,
- /// muss vorher hier durch — sonst verschwindet eine gerade getippte Änderung, weil
- /// jemand nebenbei einen Agenten angelegt hat.
- ///
- private void ApplyPendingEdits()
- {
- foreach (var agentVm in Agents)
- agentVm.ApplyChanges();
- }
-
- // ─── Teilerstellungen ───
- //
- // Die drei Teiler der Seite sollen den Neustart ueberleben. Gespeichert wird ein
- // Verhaeltnis, kein Pixelwert: Sonst sitzt der Teiler nach einem Wechsel auf einen
- // anderen Bildschirm an einer voellig anderen Stelle.
-
- public double SplitVertical => Clamp(_host?.Settings.AppSettings.AgentsSplitVertical ?? 0.55);
- public double SplitTopList => Clamp(_host?.Settings.AppSettings.AgentsSplitTopList ?? 0.44);
- public double SplitBottomList => Clamp(_host?.Settings.AppSettings.AgentsSplitBottomList ?? 0.44);
-
- ///
- /// Hält den Teiler in einem Bereich, in dem beide Seiten noch bedienbar sind. Ein
- /// gespeicherter Wert von 0.02 hätte eine Spalte auf einen Strich zusammengezogen und
- /// wäre ohne Zurücksetzen nicht mehr aufzuziehen gewesen.
- ///
- private static double Clamp(double ratio)
- => double.IsFinite(ratio) ? Math.Clamp(ratio, 0.2, 0.8) : 0.5;
-
- public void SaveSplits(double vertical, double topList, double bottomList)
- {
- if (_host is null) return;
-
- try
- {
- var settings = _host.Settings.AppSettings;
- settings.AgentsSplitVertical = Clamp(vertical);
- settings.AgentsSplitTopList = Clamp(topList);
- settings.AgentsSplitBottomList = Clamp(bottomList);
- _host.Settings.Save();
- }
- catch (Exception)
- {
- // Eine Teilerstellung ist es nicht wert, die Bedienung zu unterbrechen.
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs
deleted file mode 100644
index 92b362a..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs
+++ /dev/null
@@ -1,415 +0,0 @@
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.Core.Backup;
-using ClawdDotNet.Core.Config;
-using ClawdDotNet.Core.Storage;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.Extensions.Logging;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record BackupItemViewModel(
- string FileName,
- DateTime LastWriteTime,
- string InstanceName,
- long SizeBytes,
- string SizeFormatted,
- bool HasSecrets,
- string SecretsInfo,
- string FullPath,
- bool IsReadable
-);
-
-///
-/// Ansichtsmodell für die Sicherungs- und Wiederherstellungsseite.
-///
-public sealed partial class BackupPageViewModel : PageViewModel
-{
- private readonly BackupService _service = new();
- private readonly AppHost? _host;
-
- ///
- /// Schalter, Uhrzeit, Zielordner und Aufbewahrung stehen seit #528 (Stufe 1) hier
- /// und nicht mehr in den Anwendungseinstellungen. Dort galten sie fuer jede Instanz:
- /// Wer die taegliche Sicherung fuer eine einschaltete, schaltete sie fuer alle ein.
- ///
- private readonly InstanceConfig? _instance;
-
- private readonly ILogger? _logger;
- private readonly string _instanceDir;
- private readonly string _instanceName;
- private bool _loading;
-
- [ObservableProperty]
- private string _targetFolder = "";
-
- [ObservableProperty]
- private bool _secretsExclude = true;
-
- [ObservableProperty]
- private bool _secretsPassphrase;
-
- [ObservableProperty]
- private string _passphrase = "";
-
- [ObservableProperty]
- private string _passphraseRepeat = "";
-
- [ObservableProperty]
- private bool _includeLogs;
-
- [ObservableProperty]
- private bool _includeChatHistory = true;
-
- [ObservableProperty]
- private bool _autoBackupEnabled;
-
- [ObservableProperty]
- private string _autoBackupTime = "03:00";
-
- [ObservableProperty]
- private int _keepCount = 10;
-
- [ObservableProperty]
- private string _statusText = LocalizationManager.Instance["common.ready"];
-
- [ObservableProperty]
- private bool _isBusy;
-
- [ObservableProperty]
- private BackupItemViewModel? _selectedBackup;
-
- public ObservableCollection Backups { get; } = [];
-
- public event Func>? RequestRestoreDialog;
-
- public BackupPageViewModel() : this(null) { }
-
- public BackupPageViewModel(AppHost? host) : base("nav.backup")
- {
- _host = host;
-
- if (host is not null)
- {
- _instanceDir = host.InstancePath;
- _instanceName = host.Instance.InstanceName;
- _instance = host.Instance;
- _logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Backup");
-
- LoadSettings();
- RefreshBackupList();
- }
- else
- {
- _instanceDir = "";
- _instanceName = "Entwurf";
- TargetFolder = @"C:\ClawdDotNet\Backups";
- }
- }
-
- partial void OnAutoBackupEnabledChanged(bool value) => SaveSettings();
- partial void OnAutoBackupTimeChanged(string value) => SaveSettings();
- partial void OnKeepCountChanged(int value) => SaveSettings();
-
- private void LoadSettings()
- {
- if (_host is null || _instance is null) return;
-
- _loading = true;
- try
- {
- // Aufgeloest und nicht roh angezeigt: In der Datei steht in aller Regel
- // nichts, und ein leeres Feld beantwortet die Frage nicht, wohin gesichert
- // wird.
- TargetFolder = AppHost.ResolveBackupDirectory(_instance, _instanceDir);
- AutoBackupEnabled = _instance.AutoBackupEnabled;
- KeepCount = Math.Clamp(_instance.BackupKeepCount, 0, 999);
-
- AutoBackupTime = TimeSpan.TryParse(_instance.AutoBackupTime, out var time)
- ? time.ToString(@"hh\:mm")
- : "03:00";
- }
- finally
- {
- _loading = false;
- }
- }
-
- public void SaveSettings()
- {
- if (_loading || _host is null || _instance is null) return;
-
- var folder = TargetFolder.Trim();
-
- // Der Vorgabeort bleibt als Leereintrag stehen. Sonst schriebe die Ansicht den
- // aufgeloesten Pfad fest, und eine umgezogene Instanz sicherte weiter dorthin,
- // wo sie einmal lag.
- var vorgabe = AppHost.ResolveBackupDirectory(new InstanceConfig(), _instanceDir);
-
- _instance.BackupDirectory =
- string.Equals(folder, vorgabe, PathBoundary.Comparison) ? "" : folder;
-
- _instance.AutoBackupEnabled = AutoBackupEnabled;
- _instance.AutoBackupTime = AutoBackupTime.Trim();
- _instance.BackupKeepCount = KeepCount;
-
- _host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
- }
-
- [RelayCommand]
- private void RefreshBackupList()
- {
- Backups.Clear();
- var folder = TargetFolder.Trim();
-
- if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
- return;
-
- Task.Run(async () =>
- {
- var files = new DirectoryInfo(folder)
- .GetFiles("*.zip")
- .OrderByDescending(f => f.LastWriteTime)
- .ToList();
-
- var list = new List();
-
- foreach (var file in files)
- {
- try
- {
- var manifest = await _service.InspectAsync(file.FullName);
- var secretsInfo = manifest.HasSecrets
- ? $"ja ({manifest.SecretCount}, Passphrase)"
- : "nein";
-
- list.Add(new BackupItemViewModel(
- file.Name, file.LastWriteTime, manifest.InstanceName,
- file.Length, FormatSize(file.Length), manifest.HasSecrets, secretsInfo,
- file.FullName, true));
- }
- catch
- {
- list.Add(new BackupItemViewModel(
- file.Name, file.LastWriteTime, "—",
- file.Length, FormatSize(file.Length), false, "unlesbar",
- file.FullName, false));
- }
- }
-
- await Dispatcher.UIThread.InvokeAsync(() =>
- {
- Backups.Clear();
- foreach (var item in list)
- Backups.Add(item);
- });
- });
- }
-
- [RelayCommand]
- private async Task CreateBackupAsync()
- {
- if (string.IsNullOrWhiteSpace(_instanceDir))
- {
- StatusText = "Keine gültige Instanz geladen.";
- return;
- }
-
- var folder = TargetFolder.Trim();
- if (string.IsNullOrWhiteSpace(folder))
- {
- StatusText = "Bitte einen Zielordner angeben.";
- return;
- }
-
- BackupOptions options;
- if (SecretsPassphrase)
- {
- if (string.IsNullOrEmpty(Passphrase))
- {
- StatusText = "Bitte eine Passphrase eingeben.";
- return;
- }
-
- if (Passphrase != PassphraseRepeat)
- {
- StatusText = "Die Passphrasen stimmen nicht überein.";
- return;
- }
-
- options = new BackupOptions
- {
- Secrets = SecretMode.Passphrase,
- Passphrase = Passphrase,
- IncludeLogs = IncludeLogs,
- IncludeChatHistory = IncludeChatHistory
- };
- }
- else
- {
- options = new BackupOptions
- {
- Secrets = SecretMode.Exclude,
- IncludeLogs = IncludeLogs,
- IncludeChatHistory = IncludeChatHistory
- };
- }
-
- Directory.CreateDirectory(folder);
- var fileName = BuildFileName();
- var zipPath = Path.Combine(folder, fileName);
-
- IsBusy = true;
- StatusText = "Sicherung läuft…";
-
- try
- {
- var result = await Task.Run(() => _service.CreateAsync(_instanceDir, zipPath, options));
-
- StatusText = $"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})";
- _logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)", result.ZipPath, result.SizeBytes);
-
- Passphrase = "";
- PassphraseRepeat = "";
-
- ApplyRotation(folder);
- RefreshBackupList();
- }
- catch (Exception ex)
- {
- StatusText = $"Fehler: {ex.Message}";
- _logger?.LogError(ex, "Sicherung fehlgeschlagen");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- [RelayCommand]
- private void ShowSelectedInFolder()
- {
- if (SelectedBackup is null) return;
- SystemShell.RevealFile(SelectedBackup.FullPath);
- }
-
- [RelayCommand]
- private void DeleteSelected()
- {
- if (SelectedBackup is null) return;
-
- try
- {
- File.Delete(SelectedBackup.FullPath);
- RefreshBackupList();
- StatusText = $"Gelöscht: {SelectedBackup.FileName}";
- }
- catch (Exception ex)
- {
- StatusText = $"Konnte nicht gelöscht werden: {ex.Message}";
- }
- }
-
- [RelayCommand]
- private async Task RestoreSelectedAsync()
- {
- if (SelectedBackup is null || RequestRestoreDialog is null) return;
-
- BackupManifest manifest;
- try
- {
- manifest = await _service.InspectAsync(SelectedBackup.FullPath);
- }
- catch (Exception ex)
- {
- StatusText = $"Sicherung unlesbar: {ex.Message}";
- return;
- }
-
- var suggestedTarget = SuggestRestoreTarget(manifest);
- var vm = new RestoreBackupViewModel(manifest, suggestedTarget);
-
- var resultData = await RequestRestoreDialog(vm);
- if (resultData is null) return;
-
- IsBusy = true;
- StatusText = "Wiederherstellung läuft…";
-
- try
- {
- var restoreOptions = new RestoreOptions
- {
- Passphrase = resultData.Passphrase,
- Overwrite = resultData.Overwrite
- };
-
- var result = await Task.Run(() => _service.RestoreAsync(
- SelectedBackup.FullPath, resultData.TargetDirectory, restoreOptions));
-
- StatusText = $"{result.Written.Count} Datei(en) wiederhergestellt nach {resultData.TargetDirectory}";
- _logger?.LogInformation("Wiederherstellung: {Written} geschrieben nach {Target}",
- result.Written.Count, resultData.TargetDirectory);
- }
- catch (Exception ex)
- {
- StatusText = $"Wiederherstellung fehlgeschlagen: {ex.Message}";
- _logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
- }
- finally
- {
- IsBusy = false;
- }
- }
-
- private string BuildFileName()
- {
- var name = string.IsNullOrWhiteSpace(_instanceName) ? "Instanz" : _instanceName;
- var safeName = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
- return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
- }
-
- private string SuggestRestoreTarget(BackupManifest manifest)
- {
- var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
- var name = string.IsNullOrWhiteSpace(manifest.InstanceName) ? "Instanz" : manifest.InstanceName;
- var safeName = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
- return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
- }
-
- private void ApplyRotation(string folder)
- {
- if (KeepCount <= 0 || !Directory.Exists(folder)) return;
-
- try
- {
- var prefix = $"backup_{_instanceName}_";
- var obsolete = new DirectoryInfo(folder)
- .GetFiles("*.zip")
- .Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
- .OrderByDescending(f => f.LastWriteTime)
- .Skip(KeepCount)
- .ToList();
-
- foreach (var file in obsolete)
- {
- file.Delete();
- _logger?.LogInformation("Alte Sicherung rotiert: {Name}", file.Name);
- }
- }
- catch (Exception ex)
- {
- _logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
- }
- }
-
- private static string FormatSize(long bytes) => bytes switch
- {
- < 1024 => $"{bytes} B",
- < 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
- < 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
- _ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
- };
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs
deleted file mode 100644
index e9db1bb..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.Core.Config;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record ChatEntryItemViewModel(
- string Role,
- string Sender,
- string Text,
- DateTime Timestamp,
- bool IsUser
-);
-
-///
-/// Ansichtsmodell für die native Chat-Seite (Avalonia UI statt WebView2).
-///
-public sealed partial class ChatPageViewModel : PageViewModel
-{
- private readonly AppHost? _host;
-
- public ObservableCollection AvailableAgents { get; } = [];
- public ObservableCollection ChatEntries { get; } = [];
-
- [ObservableProperty]
- private AgentConfig? _selectedAgent;
-
- [ObservableProperty]
- private string _inputPrompt = "";
-
- [ObservableProperty]
- private bool _isSending;
-
- [ObservableProperty]
- private string _statusText = "Ready";
-
- public ChatPageViewModel() : this(null) { }
-
- public ChatPageViewModel(AppHost? host) : base("nav.chat")
- {
- _host = host;
-
- if (host is not null)
- {
- foreach (var agent in host.Instance.Agents)
- AvailableAgents.Add(agent);
-
- SelectedAgent = AvailableAgents.FirstOrDefault();
-
- if (host.Engine is not null)
- {
- host.Engine.OnChatEntryAdded += OnEngineChatEntryAdded;
- }
- }
- else
- {
- StatusText = "Design Mode";
- }
- }
-
- partial void OnSelectedAgentChanged(AgentConfig? value)
- {
- LoadChatHistory();
- }
-
- private void LoadChatHistory()
- {
- ChatEntries.Clear();
- if (_host?.Engine is null || SelectedAgent is null) return;
-
- var history = _host.Engine.GetChatHistory(SelectedAgent.AgentId);
- foreach (var entry in history)
- {
- var isUser = entry.Role.Equals("user", StringComparison.OrdinalIgnoreCase);
- ChatEntries.Add(new ChatEntryItemViewModel(
- entry.Role,
- isUser ? "You" : SelectedAgent.DisplayName,
- entry.Content,
- entry.Timestamp,
- isUser
- ));
- }
-
- StatusText = $"Chat history for '{SelectedAgent.DisplayName}' loaded ({ChatEntries.Count} messages).";
- }
-
- private void OnEngineChatEntryAdded(string agentId, string role, string text, string? toolName)
- {
- if (SelectedAgent is null || SelectedAgent.AgentId != agentId) return;
-
- Dispatcher.UIThread.Post(() =>
- {
- var isUser = role.Equals("user", StringComparison.OrdinalIgnoreCase);
- var sender = isUser ? "You" : (string.IsNullOrWhiteSpace(toolName) ? SelectedAgent.DisplayName : $"Tool: {toolName}");
-
- ChatEntries.Add(new ChatEntryItemViewModel(
- role, sender, text, DateTime.Now, isUser
- ));
- });
- }
-
- [RelayCommand]
- private async Task SendMessageAsync()
- {
- if (_host?.Engine is null || SelectedAgent is null)
- {
- StatusText = "Engine not active or no agent selected.";
- return;
- }
-
- var text = InputPrompt.Trim();
- if (string.IsNullOrWhiteSpace(text)) return;
-
- InputPrompt = "";
- IsSending = true;
- StatusText = $"{SelectedAgent.DisplayName} is thinking…";
-
- try
- {
- var agent = SelectedAgent;
- var instanceId = _host.Instance.InstanceId;
-
- var result = await Task.Run(() =>
- _host.Engine.ChatAsync(agent, text, instanceId, CancellationToken.None));
-
- StatusText = $"{agent.DisplayName} finished ({result.Status}, {result.TokensUsed:N0} tokens).";
- }
- catch (Exception ex)
- {
- StatusText = $"Error: {ex.Message}";
- }
- finally
- {
- IsSending = false;
- }
- }
-
- [RelayCommand]
- private void Abort()
- {
- if (_host?.Engine is null || SelectedAgent is null) return;
- _host.Engine.AbortChat(SelectedAgent.AgentId);
- StatusText = "Chat abgebrochen.";
- IsSending = false;
- }
-
- [RelayCommand]
- private void ClearHistory()
- {
- if (_host?.Engine is null || SelectedAgent is null) return;
- _host.Engine.ClearChatHistory(SelectedAgent.AgentId);
- ChatEntries.Clear();
- StatusText = "Verlauf geleert.";
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs
deleted file mode 100644
index e1c4aef..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System.Collections.ObjectModel;
-using System.Reflection;
-using ClawdDotNet.App;
-using ClawdDotNet.Core.Storage;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record AssemblyInfoItem(string ShortName, int BuildNumber, string BuildDate, string Changes);
-
-///
-/// Ansichtsmodell für die Info-Seite (Version, Build-Informationen, Instanz-Details).
-///
-public sealed partial class InfoPageViewModel : PageViewModel
-{
- [ObservableProperty]
- private string _appVersion = "—";
-
- [ObservableProperty]
- private string _buildSummary = "—";
-
- [ObservableProperty]
- private string _instanceName = "—";
-
- [ObservableProperty]
- private string _instanceId = "—";
-
- [ObservableProperty]
- private string _instancePath = "—";
-
- [ObservableProperty]
- private string _logDirectory = "—";
-
- [ObservableProperty]
- private string _openRouterStatus = "—";
-
- public ObservableCollection Assemblies { get; } = [];
-
- public InfoPageViewModel() : this(null) { }
-
- public InfoPageViewModel(AppHost? host) : base("nav.info")
- {
- AppVersion = AppHost.AppVersion;
- BuildSummary = AppHost.BuildSummary;
-
- if (host is not null)
- {
- InstanceName = host.Instance.InstanceName;
- InstanceId = host.Instance.InstanceId;
- InstancePath = host.InstancePath;
- LogDirectory = host.LogDirectory;
- OpenRouterStatus = host.Engine is null
- ? "Disabled (no API key configured)"
- : "Active (AgentEngine ready)";
- }
- else
- {
- InstanceName = "Design Mode";
- InstanceId = "00000000-0000-0000-0000-000000000000";
- InstancePath = @"C:\ClawdDotNet\Instances\Default";
- LogDirectory = @"C:\ClawdDotNet\Logs";
- OpenRouterStatus = "Design";
- }
-
- LoadAssemblyBuildInfos();
- }
-
- ///
- /// Fruehere Menueleiste, Punkt „Bearbeiten > Instanz-Ordner oeffnen". Der Entwurf
- /// kennt keine Menueleiste mehr; der Befehl sitzt jetzt dort, wo auch der Pfad steht.
- ///
- [RelayCommand]
- private void OpenInstanceFolder()
- {
- if (!Directory.Exists(InstancePath)) return;
- SystemShell.OpenFolder(InstancePath);
- }
-
- private void LoadAssemblyBuildInfos()
- {
- Assemblies.Clear();
-
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()
- .Where(a => a.GetName().Name?.StartsWith("ClawdDotNet") == true)
- .OrderBy(a => a.GetName().Name))
- {
- var name = asm.GetName().Name!;
- var shortName = name.Replace("ClawdDotNet.", "").Replace("Tools.", "");
-
- var buildDate = asm.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
- .OfType()
- .FirstOrDefault(a => a.Key == "BuildDate")?.Value ?? "?";
-
- var buildInfoType = asm.GetType(asm.GetName().Name + ".BuildInfo");
- var buildNum = buildInfoType?.GetField("Build")?.GetValue(null) as int? ?? 0;
- var changes = buildInfoType?.GetField("Changes")?.GetValue(null) as string ?? "—";
-
- Assemblies.Add(new AssemblyInfoItem(shortName, buildNum, buildDate, changes));
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs
deleted file mode 100644
index 17fc056..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs
+++ /dev/null
@@ -1,190 +0,0 @@
-using System.Collections.ObjectModel;
-using ClawdDotNet.App.Models;
-using ClawdDotNet.App.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using ClawdDotNet.Core.Instances;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-///
-/// Eine Instanz in der Auswahl.
-///
-/// Das Monogramm und der Punkt stammen aus dem Entwurf. Der Punkt zeigt bewusst
-/// Bereitschaft und nicht „läuft": Ob eine andere Instanz gerade läuft, weiß
-/// diese Anwendung nicht — jede Instanz ist ein eigener Prozess, und einen
-/// instanzübergreifenden Status gibt es noch nicht. Ein grüner Punkt für „läuft" wäre
-/// hier schlicht geraten.
-///
-public sealed class InstanceChoiceViewModel(InstanceInfo info, bool isCurrent)
-{
- public InstanceInfo Info { get; } = info;
-
- public string InstanceName => Info.InstanceName;
- public string FolderPath => Info.FolderPath;
-
- /// Erste zwei Buchstaben, gross — wie im Entwurf.
- public string Monogram => Info.InstanceName.Length >= 2
- ? Info.InstanceName[..2].ToUpperInvariant()
- : Info.InstanceName.ToUpperInvariant();
-
- public string Summary
- {
- get
- {
- var keyStatus = IsReady
- ? LocalizationManager.Instance["desktop.instancePicker.apiKeyConfigured"]
- : LocalizationManager.Instance["desktop.instancePicker.apiKeyMissing"];
- return string.Format(
- LocalizationManager.Instance["desktop.instancePicker.agentsCount"],
- Info.AgentCount,
- keyStatus);
- }
- }
-
- /// Ohne API-Key kann die Instanz keine Agenten starten.
- public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase)
- || Info.ApiKeyStatus.Equals("Configured", StringComparison.OrdinalIgnoreCase);
-
- /// Die gerade laufende Instanz — traegt im Entwurf das Etikett „Aktiv".
- public bool IsCurrent { get; } = isCurrent;
-}
-
-///
-/// Auswahl der Instanz — beim Start und beim Wechseln aus der laufenden Anwendung.
-///
-/// Die WinForms-Fassung mischte Datenzugriff, Spaltenformatierung und Meldungsfenster
-/// in einem Formular. Hier steht nur, was geschieht; das wie es aussieht
-/// liegt in der Ansicht, und Rückfragen laufen über statt über
-/// einen direkten Dialogaufruf — so bleibt das Ansichtsmodell ohne Fensterbezug prüfbar.
-///
-public sealed partial class InstancePickerViewModel : ViewModelBase
-{
- private readonly InstanceDirectoryManager _directories;
- private readonly TaskCompletionSource _completion = new();
- private readonly string? _currentInstancePath;
-
- [ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(StartCommand))]
- private InstanceChoiceViewModel? _selected;
-
- [ObservableProperty]
- private string _newInstanceName = "";
-
- /// Fehlertext für die Ansicht; leer heißt: alles in Ordnung.
- [ObservableProperty]
- private string _errorText = "";
-
- public ObservableCollection Instances { get; } = [];
-
- /// Der gewählte Pfad, oder null bei Abbruch.
- public Task Result => _completion.Task;
-
- public string InstancesDirectory => _directories.InstancesDirectory;
-
- ///
- /// Beim Start heisst die Schaltflaeche „Starten", aus der laufenden Anwendung heraus
- /// „Wechseln" — und dann steht der Hinweis dabei, dass dafuer neu gestartet wird.
- ///
- public bool IsSwitchMode => _currentInstancePath is not null;
-
- public string ConfirmLabel => IsSwitchMode
- ? LocalizationManager.Instance["desktop.instancePicker.confirmSwitch"]
- : LocalizationManager.Instance["desktop.instancePicker.confirmStart"];
-
- /// Ueberschrift des Dialogs — nicht aus ConfirmLabel zusammengesetzt,
- /// sonst stuende dort „Instanz Starten".
- public string Title => IsSwitchMode
- ? LocalizationManager.Instance["desktop.instancePicker.titleSwitch"]
- : LocalizationManager.Instance["desktop.instancePicker.titleSelect"];
-
- public string Hint => IsSwitchMode
- ? LocalizationManager.Instance["desktop.instancePicker.hintSwitch"]
- : LocalizationManager.Instance["desktop.instancePicker.hintSelect"];
-
- ///
- /// Pfad der laufenden Instanz, oder null beim Start. Steuert Beschriftung und
- /// das Etikett „Aktiv" in der Liste.
- ///
- public InstancePickerViewModel(InstanceDirectoryManager directories, string? currentInstancePath = null)
- {
- _directories = directories;
- _currentInstancePath = currentInstancePath;
- Refresh();
- }
-
- [RelayCommand]
- private void Refresh()
- {
- Instances.Clear();
-
- foreach (var instance in _directories.ListInstances())
- {
- var isCurrent = _currentInstancePath is not null
- && string.Equals(instance.FolderPath, _currentInstancePath,
- StringComparison.OrdinalIgnoreCase);
-
- Instances.Add(new InstanceChoiceViewModel(instance, isCurrent));
- }
-
- // Eine einzelne Instanz gleich vorwählen — der häufigste Fall, und ein Klick
- // weniger bei jedem Start.
- Selected ??= Instances.FirstOrDefault(i => i.IsCurrent) ?? Instances.FirstOrDefault();
- }
-
- private bool CanStart => Selected is not null;
-
- [RelayCommand(CanExecute = nameof(CanStart))]
- private void Start() => _completion.TrySetResult(Selected!.FolderPath);
-
- /// Waehlt eine Zeile und bestaetigt in einem Zug — die Schaltflaeche je Zeile.
- [RelayCommand]
- private void Activate(InstanceChoiceViewModel? choice)
- {
- if (choice is null) return;
- Selected = choice;
- _completion.TrySetResult(choice.FolderPath);
- }
-
- /// Abbruch — der Aufrufer beendet die Anwendung oder schliesst den Dialog.
- [RelayCommand]
- private void Cancel() => _completion.TrySetResult(null);
-
- [RelayCommand]
- private void Create()
- {
- var name = NewInstanceName.Trim();
-
- if (string.IsNullOrWhiteSpace(name))
- {
- ErrorText = LocalizationManager.Instance["desktop.instancePicker.nameRequired"];
- return;
- }
-
- try
- {
- _directories.CreateInstance(name);
- NewInstanceName = "";
- ErrorText = "";
- Refresh();
-
- // Die frisch angelegte Instanz vorwählen — wer sie erstellt, will sie starten.
- Selected = Instances.FirstOrDefault(i =>
- string.Equals(i.InstanceName, name, StringComparison.Ordinal));
- }
- catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
- or ArgumentException)
- {
- ErrorText = string.Format(
- LocalizationManager.Instance["desktop.instancePicker.createFailed"],
- ex.Message);
- }
- }
-
- ///
- /// Wird gesetzt, wenn ein Fehler von außen kommt (etwa eine unlesbare
- /// Instanzkonfiguration), damit das Fenster offen bleiben und eine andere Wahl
- /// zulassen kann.
- ///
- public void ConfirmError(string message) => ErrorText = message;
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/LicenseViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/LicenseViewModel.cs
deleted file mode 100644
index 5cdfad1..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/LicenseViewModel.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-/// Eingabe eines Lizenzschlüssels. Siehe AvaloniaLicensePrompt.
-public sealed partial class LicenseViewModel : ViewModelBase
-{
- private readonly TaskCompletionSource _completion = new();
-
- [ObservableProperty]
- private string _key;
-
- public string HardwareId { get; }
-
- /// Warum der bisherige Schlüssel nicht taugt; leer beim ersten Fragen.
- public string Problem { get; }
-
- /// Der eingegebene Schlüssel, oder null bei Abbruch.
- public Task Result => _completion.Task;
-
- public LicenseViewModel(string hardwareId, string? problem, string? currentKey)
- {
- HardwareId = hardwareId;
- Problem = problem ?? "";
- _key = currentKey ?? "";
- }
-
- [RelayCommand]
- private void Confirm()
- {
- var value = Key.Trim();
-
- // Leere Eingabe ist kein Abbruch — sonst schließt sich das Fenster, wenn jemand
- // versehentlich die Eingabetaste drückt.
- if (value.Length == 0) return;
-
- _completion.TrySetResult(value);
- }
-
- [RelayCommand]
- private void Cancel() => _completion.TrySetResult(null);
-
- /// Schließt der Benutzer das Fenster, gilt das als Abbruch.
- public void CancelIfPending() => _completion.TrySetResult(null);
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs
deleted file mode 100644
index a34f12c..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.Core.Storage;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-///
-/// Live-Ansicht der Logdateien.
-///
-/// Der Takt läuft über — das Avalonia-Gegenstück zum
-/// System.Windows.Forms.Timer, also auf dem Oberflächenfaden. Für eine Ansicht
-/// ist das genau richtig: Das Lesen selbst dauert Millisekunden, und so entfällt jedes
-/// Marshalling. Alles, was ohne Fenster laufen muss, liegt dagegen in
-/// und taktet dort über PeriodicTimer.
-///
-public sealed partial class LogPageViewModel : PageViewModel
-{
- ///
- /// Obergrenze der angezeigten Zeilen.
- ///
- /// Ohne Deckel wächst die Liste unbegrenzt — ein Agent, der eine Nacht durchläuft,
- /// hinterlässt sonst Hunderttausende Einträge im Speicher, und das Scrollen wird zäh.
- ///
- private const int MaxLines = 2000;
-
- private readonly LogTail? _tail;
- private readonly DispatcherTimer? _timer;
- private readonly string _logDirectory;
-
- public ObservableCollection Lines { get; } = [];
-
- public ObservableCollection Modules { get; } = [LocalizationManager.Instance["common.all"]];
-
- public IReadOnlyList Levels { get; } = [LocalizationManager.Instance["common.all"], "Info", "Warn", "Error"];
-
- [ObservableProperty]
- private string _selectedModule = LocalizationManager.Instance["common.all"];
-
- [ObservableProperty]
- private string _selectedLevel = LocalizationManager.Instance["common.all"];
-
- ///
- /// Hängt die Ansicht am unteren Ende? Sobald jemand nach oben scrollt, soll nicht
- /// weitergesprungen werden — sonst kann man nichts lesen, während etwas läuft.
- ///
- [ObservableProperty]
- private bool _followTail = true;
-
- public LogPageViewModel(AppHost? host) : base("nav.logs")
- {
- _logDirectory = host?.LogDirectory ?? "";
-
- if (host is null) return; // Entwurfsmodus
-
- _tail = new LogTail(_logDirectory);
- RefreshModules();
-
- _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
- _timer.Tick += (_, _) => Poll();
- _timer.Start();
- }
-
- private void Poll()
- {
- if (_tail is null) return;
-
- var isAllModule = string.Equals(SelectedModule, LocalizationManager.Instance["common.all"], StringComparison.OrdinalIgnoreCase) ||
- string.Equals(SelectedModule, "All", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(SelectedModule, "Alle", StringComparison.OrdinalIgnoreCase);
- var module = isAllModule ? null : SelectedModule;
- var level = SelectedLevel switch
- {
- "Info" => LogLevelFilter.Info,
- "Warn" => LogLevelFilter.Warn,
- "Error" => LogLevelFilter.Error,
- _ => LogLevelFilter.All
- };
-
- foreach (var line in _tail.ReadNew(module, level))
- {
- Lines.Add(line);
-
- // Von vorn kürzen statt am Ende zu deckeln: Das Neueste soll bleiben.
- while (Lines.Count > MaxLines)
- Lines.RemoveAt(0);
- }
- }
-
- [RelayCommand]
- private void Clear() => Lines.Clear();
-
- [RelayCommand]
- private void RefreshModules()
- {
- if (_tail is null) return;
-
- var current = SelectedModule;
- var allText = LocalizationManager.Instance["common.all"];
-
- Modules.Clear();
- Modules.Add(allText);
-
- foreach (var module in _tail.AvailableModules())
- Modules.Add(module);
-
- // Auswahl halten, wenn es das Modul noch gibt.
- SelectedModule = Modules.Contains(current) ? current : allText;
- }
-
- [RelayCommand]
- private void OpenFolder() => SystemShell.OpenFolder(_logDirectory);
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs
deleted file mode 100644
index 8003868..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs
+++ /dev/null
@@ -1,396 +0,0 @@
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.Core.Storage;
-using ClawdDotNet.Desktop.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-// Ohne Alias verweist "App" hier auf ClawdDotNet.Desktop.App, die Anwendungsklasse
-// von Avalonia — und nicht auf die gleichnamige Fachschicht.
-using AppSettings = ClawdDotNet.App.Settings.AppSettings;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-///
-/// Das Gerüst des Hauptfensters: Seitenleiste, Kopfzeile, Meldungen, Statusleiste.
-///
-/// Die Menüleiste ist entfallen. Der Entwurf kennt keine, und ihre Einträge hatten
-/// ohnehin überall sonst schon einen Platz: Der Instanzwechsel sitzt jetzt als eigener
-/// Schalter oben in der Seitenleiste, die Ordner-Befehle stehen dort, wo die Ordner
-/// gebraucht werden (Logs-Seite, Info-Seite), und „Ansicht" war eine zweite Navigation
-/// neben der Navigation.
-///
-public sealed partial class MainWindowViewModel : ViewModelBase
-{
- private readonly AppHost? _host;
-
- /// Breite der Seitenleiste, ausgeklappt und eingeklappt (siehe Entwurf).
- private const double SidebarWidthExpanded = 240;
- private const double SidebarWidthCollapsed = 64;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(PageTitle))]
- private PageViewModel? _selectedPage;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(SidebarWidth))]
- [NotifyPropertyChangedFor(nameof(IsSidebarExpanded))]
- private bool _isSidebarCollapsed;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(ThemeLabel))]
- [NotifyPropertyChangedFor(nameof(ThemeIconKey))]
- private bool _isDarkTheme;
-
- // ─── Statusleiste ───
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(InstanceStatusLabel))]
- private string _instanceName = "—";
-
- [ObservableProperty]
- private bool _isApiReachable;
-
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(ApiStatusDisplay))]
- private string _apiStatusText = "unbekannt";
-
- public string InstanceStatusLabel => LocalizationManager.Instance.Get("desktop.mainWindow.instanceLabel", InstanceName);
- public string ApiStatusDisplay => LocalizationManager.Instance.Get("desktop.mainWindow.apiStatus", ApiStatusText);
-
- /// Kurzinfo zu Tokens und Kosten der letzten Stunde bzw. 24 Stunden.
- [ObservableProperty]
- private string _usageText = "—";
-
- [ObservableProperty]
- private string _usageTooltip = "";
-
- [ObservableProperty]
- private string _creditsText = "—";
-
- /// Alle Bereiche, in der Reihenfolge der Navigation.
- public ObservableCollection Pages { get; } = [];
-
- /// Die Navigation, gruppiert wie im Entwurf.
- public ObservableCollection NavGroups { get; } = [];
-
- ///
- /// Instanzübergreifende Hinweise. Bleibt vorerst leer — der Dienst, der Fehler und
- /// aufgebrauchte Guthaben anderer Instanzen sieht, existiert noch nicht
- /// (siehe Implementierungsleitfaden im Mockup-Verzeichnis). Die Leiste blendet sich
- /// aus, solange nichts anliegt, und ist damit heute schon unauffällig richtig.
- ///
- public ObservableCollection Notifications { get; } = [];
-
- public bool HasNotifications => Notifications.Count > 0;
-
- public double SidebarWidth => IsSidebarCollapsed ? SidebarWidthCollapsed : SidebarWidthExpanded;
-
- public bool IsSidebarExpanded => !IsSidebarCollapsed;
-
- public string PageTitle => SelectedPage?.Title ?? "";
-
- public string ThemeLabel => IsDarkTheme
- ? LocalizationManager.Instance["desktop.mainWindow.themeDark"]
- : LocalizationManager.Instance["desktop.mainWindow.themeLight"];
-
- /// Fusszeile der Seitenleiste.
- public string VersionText => $"ClawdDotNet {AppHost.AppVersion} · Avalonia";
-
- /// Sonne im Hellmodus, Mond im Dunkelmodus — beides Namen aus Icons.axaml.
- public string ThemeIconKey => IsDarkTheme ? "IconMoon" : "IconSun";
-
- public event Action? RequestExit;
- public event Action? RequestInstanceManagerDialog;
-
- ///
- /// Ob der Instanzwechsel aus diesem Fenster heraus angeboten wird.
- ///
- /// false, sobald der Instanzmanager diese Instanz führt (#528, E-M2):
- /// Dann hält er die Fenster aller Instanzen im Hintergrund und holt das gewünschte
- /// nach vorn. Ein Wechsel von hier aus wäre ein Selbst-Neustart und nähme genau die
- /// Instanz mit, aus der heraus jemand wechselt.
- ///
- public bool CanSwitchInstance { get; init; } = true;
-
- /// Für den Entwurfsmodus des Editors — ohne laufenden Aufbau.
- public MainWindowViewModel() : this(null) { }
-
- public MainWindowViewModel(AppHost? host)
- {
- _host = host;
-
- var chat = new ChatPageViewModel(host);
- var agents = new AgentsPageViewModel(host);
- var tasks = new TasksPageViewModel(host);
- var tokens = new TokenUsagePageViewModel(host);
- var agentChats = new AgentChatsPageViewModel(host);
- var backup = new BackupPageViewModel(host);
- var logs = new LogPageViewModel(host);
- var settings = new SettingsPageViewModel(host);
- var info = new InfoPageViewModel(host);
-
- foreach (var page in new PageViewModel[]
- { chat, agents, tasks, tokens, agentChats, backup, logs, settings, info })
- Pages.Add(page);
-
- NavGroups.Add(new NavGroupViewModel("desktop.nav.work",
- [
- new NavItemViewModel(this, chat, "IconChat"),
- new NavItemViewModel(this, agents, "IconAgents"),
- new NavItemViewModel(this, tasks, "IconTasks")
- ]));
-
- NavGroups.Add(new NavGroupViewModel("desktop.nav.analytics",
- [
- new NavItemViewModel(this, tokens, "IconChart"),
- new NavItemViewModel(this, agentChats, "IconAgentChats")
- ]));
-
- NavGroups.Add(new NavGroupViewModel("desktop.nav.system",
- [
- new NavItemViewModel(this, backup, "IconBackup"),
- new NavItemViewModel(this, logs, "IconLogs"),
- new NavItemViewModel(this, settings, "IconSettings"),
- new NavItemViewModel(this, info, "IconInfo")
- ]));
-
- LocalizationManager.Instance.LanguageChanged += () =>
- {
- OnPropertyChanged(nameof(PageTitle));
- OnPropertyChanged(nameof(ThemeLabel));
- OnPropertyChanged(nameof(InstanceStatusLabel));
- OnPropertyChanged(nameof(ApiStatusDisplay));
- if (_host is null)
- {
- InstanceName = LocalizationManager.Instance["desktop.status.design"];
- ApiStatusText = LocalizationManager.Instance["desktop.status.design"];
- }
- };
-
- // Die Leiste blendet sich aus, solange nichts anliegt — dafür muss die Ansicht
- // von Zu- und Abgängen erfahren, nicht nur von der Sammlung selbst.
- Notifications.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasNotifications));
-
- SelectedPage = chat;
-
- if (host is null)
- {
- InstanceName = "Entwurfsmodus";
- ApiStatusText = "Entwurf";
- UsageText = "1h: 3.240 Tok · 24h: 168.500 Tok";
- CreditsText = "Credits: $41.86 von $100.00";
-
- // Zwei Beispielmeldungen, damit die Leiste im Editor sichtbar ist.
- Notifications.Add(new NotificationViewModel(
- this, isError: true, "sandbox", "OpenRouter-Guthaben aufgebraucht — Agenten pausiert."));
- Notifications.Add(new NotificationViewModel(
- this, isError: false, "stock-team", "3 unbehandelte Exceptions in den letzten 15 Minuten."));
- return;
- }
-
- InstanceName = host.Instance.InstanceName;
-
- IsSidebarCollapsed = host.Settings.AppSettings.SidebarCollapsed;
- IsDarkTheme = ThemeManager.IsDarkName(host.Settings.AppSettings.Theme);
- ThemeManager.Apply(IsDarkTheme);
- ApplyCollapsedToNav();
-
- HookStatusService(host);
- }
-
- ///
- /// Hängt die Statusleiste an den OpenRouter-Dienst.
- ///
- /// Der Dienst war fertig und ungenutzt — er lieferte seine Texte bislang an
- /// niemanden. OnStatusUpdated kommt vom Hintergrundfaden des
- /// PeriodicTimer, deshalb der Wechsel auf den Oberflächenfaden.
- ///
- private void HookStatusService(AppHost host)
- {
- if (host.Status is not { } status)
- {
- ApiStatusText = LocalizationManager.Instance["desktop.status.noApiKey"];
- UsageText = LocalizationManager.Instance["desktop.status.noUsageData"];
- CreditsText = "";
- return;
- }
-
- void Update()
- {
- IsApiReachable = status.IsApiReachable;
- ApiStatusText = status.IsApiReachable
- ? LocalizationManager.Instance["desktop.status.reachable"]
- : LocalizationManager.Instance["desktop.status.unreachable"];
- UsageText = status.CreditsText;
- UsageTooltip = status.CreditsTooltip;
-
- CreditsText = status.CreditRemaining is { } remaining && status.CreditBalance is { } total
- ? LocalizationManager.Instance.Get("desktop.mainWindow.credits", remaining, total)
- : LocalizationManager.Instance["desktop.mainWindow.creditsNone"];
- }
-
- status.OnStatusUpdated += () => Dispatcher.UIThread.Post(Update);
- Update();
- }
-
- partial void OnSelectedPageChanged(PageViewModel? value)
- {
- foreach (var item in NavGroups.SelectMany(g => g.Items))
- item.IsSelected = ReferenceEquals(item.Page, value);
- }
-
- partial void OnIsSidebarCollapsedChanged(bool value)
- {
- ApplyCollapsedToNav();
- Persist(s => s.SidebarCollapsed = value);
- }
-
- partial void OnIsDarkThemeChanged(bool value)
- {
- ThemeManager.Apply(value);
- Persist(s => s.Theme = ThemeManager.ToName(value));
- }
-
- ///
- /// Der eingeklappte Zustand steht an jedem Eintrag statt an einer Bindung auf das
- /// Elternfenster: Aus einer DataTemplate heraus zurück auf das Fenster zu binden ist
- /// mit übersetzten Bindungen jedes Mal ein Ausdruck, den niemand mehr liest.
- ///
- private void ApplyCollapsedToNav()
- {
- foreach (var group in NavGroups)
- {
- group.IsTitleVisible = !IsSidebarCollapsed;
-
- foreach (var item in group.Items)
- item.IsCollapsed = IsSidebarCollapsed;
- }
- }
-
- ///
- /// Einstellungen sind ein Nebeneffekt der Bedienung, kein eigener Vorgang: Wer die
- /// Leiste einklappt, drückt nicht anschließend auf Speichern. Ein Schreibfehler darf
- /// die Bedienung deshalb auch nicht unterbrechen.
- ///
- private void Persist(Action change)
- {
- if (_host is null) return;
-
- try
- {
- change(_host.Settings.AppSettings);
- _host.Settings.Save();
- }
- catch (Exception)
- {
- // Der Zustand gilt für diese Sitzung trotzdem — nur eben nicht für die nächste.
- }
- }
-
- [RelayCommand]
- private void SelectPage(PageViewModel? page)
- {
- if (page is not null) SelectedPage = page;
- }
-
- [RelayCommand]
- private void ToggleSidebar() => IsSidebarCollapsed = !IsSidebarCollapsed;
-
- [RelayCommand]
- private void ToggleTheme() => IsDarkTheme = !IsDarkTheme;
-
- [RelayCommand]
- private void OpenInstanceFolder()
- {
- if (_host is null) return;
- SystemShell.OpenFolder(_host.InstancePath);
- }
-
- [RelayCommand]
- private void OpenInstanceManager() => RequestInstanceManagerDialog?.Invoke();
-
- [RelayCommand]
- private void Exit() => RequestExit?.Invoke();
-}
-
-/// Eine Gruppe der Navigation („Arbeiten", „Analyse", „System").
-public sealed partial class NavGroupViewModel : ObservableObject
-{
- private readonly string _titleKey;
- public string Title => LocalizationManager.Instance.Get(_titleKey);
- public IReadOnlyList Items { get; }
-
- /// Im eingeklappten Zustand entfällt die Gruppenüberschrift.
- [ObservableProperty]
- private bool _isTitleVisible = true;
-
- public NavGroupViewModel(string titleKey, IReadOnlyList items)
- {
- _titleKey = titleKey;
- Items = items;
- LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
- }
-}
-
-///
-/// Ein Eintrag der Navigation.
-///
-/// Der Befehl sitzt am Eintrag selbst und nicht am Fenster: Aus einer DataTemplate
-/// heraus über $parent[…] auf einen Befehl des Fensters zu binden funktioniert
-/// zwar, ist aber bei übersetzten Bindungen ein Ausdruck mit Typumwandlung, den man beim
-/// nächsten Lesen erst wieder entziffern muss.
-///
-public sealed partial class NavItemViewModel : ObservableObject
-{
- private readonly MainWindowViewModel _owner;
- public PageViewModel Page { get; }
- public string Title => Page.Title;
-
- /// Name der Geometrie in Styles/Icons.axaml.
- public string IconKey { get; }
-
- [ObservableProperty]
- private bool _isSelected;
-
- [ObservableProperty]
- private bool _isCollapsed;
-
- public NavItemViewModel(MainWindowViewModel owner, PageViewModel page, string iconKey)
- {
- _owner = owner;
- Page = page;
- IconKey = iconKey;
- LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
- }
-
- [RelayCommand]
- private void Select() => _owner.SelectedPage = Page;
-}
-
-/// Eine Zeile der Benachrichtigungsleiste.
-public sealed partial class NotificationViewModel(
- MainWindowViewModel owner, bool isError, string instanceName, string text) : ObservableObject
-{
- public bool IsError { get; } = isError;
- public bool IsWarning => !IsError;
- public string InstanceName { get; } = instanceName;
- public string Text { get; } = text;
-
- [RelayCommand]
- private void Dismiss() => owner.Notifications.Remove(this);
-}
-
-/// Ein Bereich des Hauptfensters.
-public abstract partial class PageViewModel : ViewModelBase
-{
- private readonly string _titleKey;
- public string Title => LocalizationManager.Instance.Get(_titleKey);
-
- protected PageViewModel(string titleKey)
- {
- _titleKey = titleKey;
- LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs
deleted file mode 100644
index d93bf37..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using ClawdDotNet.Core.Backup;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record RestoreResultData(string TargetDirectory, string? Passphrase, bool Overwrite);
-
-public sealed partial class RestoreBackupViewModel : ViewModelBase
-{
- private readonly BackupManifest _manifest;
-
- public string InstanceName => _manifest.InstanceName;
- public DateTime CreatedAt => _manifest.CreatedAt;
- public int FileCount => _manifest.Files.Count;
- public bool NeedsPassphrase => _manifest.HasSecrets;
-
- public string SecretsDescription => _manifest.HasSecrets
- ? $"{_manifest.SecretCount}, passphrase protected"
- : _manifest.SecretCount > 0
- ? $"{_manifest.SecretCount} excluded"
- : "none";
-
- public string InstanceInfoText => string.Format(L["desktop.dialog.backupInstance"], InstanceName);
- public string CreatedAtText => string.Format(L["desktop.dialog.backupCreated"], CreatedAt);
- public string FileCountText => string.Format(L["desktop.dialog.backupFiles"], FileCount);
- public string SecretsText => string.Format(L["desktop.dialog.backupSecrets"], SecretsDescription);
-
- [ObservableProperty]
- private string _targetDirectory = "";
-
- [ObservableProperty]
- private string _passphrase = "";
-
- [ObservableProperty]
- private bool _overwrite;
-
- [ObservableProperty]
- private string _statusMessage = "";
-
- public RestoreResultData? Result { get; private set; }
-
- public event Action? CloseRequested;
-
- public RestoreBackupViewModel() : this(new BackupManifest(), @"C:\Instance_Restored") { }
-
- public RestoreBackupViewModel(BackupManifest manifest, string suggestedTarget)
- {
- _manifest = manifest;
- TargetDirectory = suggestedTarget;
- }
-
- [RelayCommand]
- private void Confirm()
- {
- if (string.IsNullOrWhiteSpace(TargetDirectory))
- {
- StatusMessage = "Please specify a target directory.";
- return;
- }
-
- if (NeedsPassphrase && string.IsNullOrEmpty(Passphrase))
- {
- StatusMessage = "Please enter the passphrase for protected credentials.";
- return;
- }
-
- Result = new RestoreResultData(
- TargetDirectory.Trim(),
- NeedsPassphrase ? Passphrase : null,
- Overwrite);
-
- CloseRequested?.Invoke();
- }
-
- [RelayCommand]
- private void Cancel()
- {
- Result = null;
- CloseRequested?.Invoke();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs
deleted file mode 100644
index adcca0a..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.Extensions.Logging;
-using ClawdDotNet.Desktop.Services;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-///
-/// Ansichtsmodell für die Einstellungen-Seite (Anwendungs- und Instanz-Einstellungen).
-///
-public sealed partial class SettingsPageViewModel : PageViewModel
-{
- private readonly AppHost? _host;
- private readonly ILogger? _logger;
-
- /// Womit die Seite geladen wurde — nur, um eine Änderung zu erkennen.
- private string _logDirectoryAtLoad = "";
-
- // ─── Allgemein & Pfade ───
- [ObservableProperty]
- private string _instancesDirectory = "";
-
- [ObservableProperty]
- private string _minimumLogLevel = "Info";
-
- [ObservableProperty]
- private int _maxLogLinesInUi = 2000;
-
- [ObservableProperty]
- private int _logRefreshIntervalMs = 500;
-
- // ─── API & OpenRouter ───
- [ObservableProperty]
- private string _openRouterBaseUrl = "https://openrouter.ai/api/v1/";
-
- [ObservableProperty]
- private int _statusCheckIntervalSeconds = 60;
-
- // Die Sicherungs-Einstellungen (Zielordner, Uhrzeit, Anzahl) stehen bewusst
- // NICHT hier, sondern allein auf der Sicherungs-Seite. Sie lagen frueher
- // doppelt vor: Die Felder waren aus der Ansicht bereits verschwunden, wurden
- // aber weiter beim Start gelesen und beim Speichern zurueckgeschrieben - wer
- // die Uhrzeit auf der Sicherungs-Seite aenderte und danach hier speicherte,
- // bekam den alten Wert zurueck.
- // ─── Deploymentcenter ───
- [ObservableProperty]
- private string _deploymentcenterUrl = "https://dc.mhdf.de";
-
- [ObservableProperty]
- private string _deploymentcenterToken = "";
-
- [ObservableProperty]
- private string _deploymentcenterEnvironment = "production";
-
- [ObservableProperty]
- private bool _errorReportingEnabled = true;
-
- [ObservableProperty]
- private bool _updateCheckEnabled = true;
-
- [ObservableProperty]
- private string _updateChannel = "prod";
-
- // ─── Lizenz ───
- [ObservableProperty]
- private string _licenseKey = "";
-
- // ─── Instanz-Einstellungen ───
- [ObservableProperty]
- private string _instanceName = "";
-
- ///
- /// Wohin diese Instanz protokolliert. Steht seit #528 (Stufe 1) hier und nicht mehr
- /// bei den Anwendungseinstellungen: Ein Verzeichnis für alle hiess, dass in
- /// Startup.log abwechselnd zwei Instanzen schrieben, ohne Kennzeichnung, welche
- /// gerade spricht.
- ///
- /// Leer angezeigt heisst leer gemeint — die Vorgabe steht als Hinweis unter
- /// dem Feld. Anders als bei der Sicherungsseite wird hier bewusst nicht aufgeloest:
- /// Der geltende Ort steht auf der Info-Seite, und ein aufgeloester Pfad im Feld
- /// wuerde beim naechsten Speichern festgeschrieben.
- ///
- [ObservableProperty]
- private string _logDirectory = "";
-
- ///
- /// Ob der Instanzmanager diese Instanz am Laufen hält (#528).
- ///
- /// Steht hier und nicht beim Manager, weil es eine Eigenschaft dieser Instanz
- /// ist — und weil die Instanzdatei dem Instanzprozess gehört: Sie vom Manager
- /// schreiben zu lassen, während die Instanz läuft, waeren zwei Schreiber auf einer
- /// Datei. Der Manager liest den Schalter nur.
- ///
- [ObservableProperty]
- private bool _autostartByManager;
-
- [ObservableProperty]
- private string _openRouterApiKey = "";
-
- [ObservableProperty]
- private decimal _instanceDailyCostUsd;
-
- [ObservableProperty]
- private long _instanceDailyTokens;
-
- [ObservableProperty]
- private string _statusText = "Bereit";
-
- [ObservableProperty]
- private string _selectedLanguage = LocalizationManager.DefaultLanguage;
-
- public IReadOnlyList AvailableLanguages => LocalizationManager.Instance.AvailableLanguages;
-
- public LanguageOption? SelectedLanguageOption
- {
- get => AvailableLanguages.FirstOrDefault(l => l.Code.Equals(SelectedLanguage, StringComparison.OrdinalIgnoreCase)) ?? AvailableLanguages.FirstOrDefault();
- set
- {
- if (value is not null && value.Code != SelectedLanguage)
- {
- SelectedLanguage = value.Code;
- OnPropertyChanged();
- LocalizationManager.Instance.SetLanguage(value.Code);
- }
- }
- }
-
- public List LogLevels { get; } = ["Debug", "Info", "Warn", "Error"];
- public List UpdateChannels { get; } = ["prod", "beta", "dev"];
- public List Environments { get; } = ["production", "development", "staging"];
-
- public SettingsPageViewModel() : this(null) { }
-
- public SettingsPageViewModel(AppHost? host) : base("nav.settings")
- {
- _host = host;
-
- if (host is not null)
- {
- _logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Settings");
- LoadFromHost();
- }
- else
- {
- StatusText = LocalizationManager.Instance["desktop.status.design"];
- }
- }
-
- [RelayCommand]
- private void LoadFromHost()
- {
- if (_host is null) return;
-
- var s = _host.Settings.AppSettings;
- InstancesDirectory = s.InstancesDirectory;
- MinimumLogLevel = s.MinimumLogLevel;
- MaxLogLinesInUi = s.MaxLogLinesInUi;
- LogRefreshIntervalMs = s.LogRefreshIntervalMs;
- OpenRouterBaseUrl = s.OpenRouterBaseUrl;
- StatusCheckIntervalSeconds = s.StatusCheckIntervalSeconds;
- DeploymentcenterUrl = s.DeploymentcenterUrl;
- DeploymentcenterToken = s.DeploymentcenterToken;
- DeploymentcenterEnvironment = s.DeploymentcenterEnvironment;
- ErrorReportingEnabled = s.ErrorReportingEnabled;
- UpdateCheckEnabled = s.UpdateCheckEnabled;
- UpdateChannel = s.UpdateChannel;
- LicenseKey = s.LicenseKey;
- SelectedLanguage = s.Language ?? LocalizationManager.DefaultLanguage;
- OnPropertyChanged(nameof(SelectedLanguageOption));
-
- var inst = _host.Instance;
- InstanceName = inst.InstanceName;
- LogDirectory = inst.LogDirectory;
- AutostartByManager = inst.AutostartByManager;
- _logDirectoryAtLoad = inst.LogDirectory;
- OpenRouterApiKey = inst.OpenRouterApiKey;
- InstanceDailyCostUsd = inst.Budget.DailyCostUsd;
- InstanceDailyTokens = inst.Budget.DailyTokens;
- }
-
- [RelayCommand]
- private void SaveSettings()
- {
- if (_host is null) return;
-
- var s = _host.Settings.AppSettings;
- s.InstancesDirectory = InstancesDirectory.Trim();
- s.MinimumLogLevel = MinimumLogLevel;
- s.MaxLogLinesInUi = MaxLogLinesInUi;
- s.LogRefreshIntervalMs = LogRefreshIntervalMs;
- s.OpenRouterBaseUrl = OpenRouterBaseUrl.Trim();
- s.StatusCheckIntervalSeconds = StatusCheckIntervalSeconds;
- s.DeploymentcenterUrl = DeploymentcenterUrl.Trim();
- s.DeploymentcenterToken = DeploymentcenterToken.Trim();
- s.DeploymentcenterEnvironment = DeploymentcenterEnvironment.Trim();
- s.ErrorReportingEnabled = ErrorReportingEnabled;
- s.UpdateCheckEnabled = UpdateCheckEnabled;
- s.UpdateChannel = UpdateChannel;
- s.LicenseKey = LicenseKey.Trim();
- s.Language = SelectedLanguage;
-
- _host.Settings.Save();
- LocalizationManager.Instance.SetLanguage(SelectedLanguage);
-
- var inst = _host.Instance;
- inst.InstanceName = InstanceName.Trim();
- inst.LogDirectory = LogDirectory.Trim();
- inst.AutostartByManager = AutostartByManager;
- inst.OpenRouterApiKey = OpenRouterApiKey.Trim();
- inst.Budget.DailyCostUsd = InstanceDailyCostUsd;
- inst.Budget.DailyTokens = InstanceDailyTokens;
-
- _host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
-
- _logger?.LogInformation("Settings and instance configuration saved.");
-
- // Das Protokoll-Verzeichnis greift erst beim naechsten Start: Die
- // Protokollierung steht seit dem Aufbau des AppHost und schreibt bereits.
- StatusText = string.Equals(inst.LogDirectory, _logDirectoryAtLoad, StringComparison.Ordinal)
- ? LocalizationManager.Instance["desktop.settings.saved"]
- : LocalizationManager.Instance["desktop.settings.savedLogRestart"];
-
- _logDirectoryAtLoad = inst.LogDirectory;
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs
deleted file mode 100644
index 9051afc..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs
+++ /dev/null
@@ -1,398 +0,0 @@
-using System.Collections.ObjectModel;
-using Avalonia.Threading;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Models;
-using ClawdDotNet.App.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using Microsoft.Extensions.Logging;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed record JobDisplayItemViewModel(
- string JobType,
- string JobId,
- string AgentId,
- string AgentName,
- string ToolName,
- string CronExpression,
- string TaskMessage,
- bool RunOnStart,
- string NextRun,
- string LastRun,
- string LastStatus,
- string Status
-);
-
-/// Die drei Ansichten der Aufgaben-Seite.
-public enum TasksTab
-{
- Jobs,
- Services,
- History
-}
-
-public sealed record ServiceDisplayItemViewModel(
- string ServiceId,
- string Name,
- string Type,
- int Port,
- string Status,
- string Description
-);
-
-///
-/// Ansichtsmodell für die Aufgaben-Seite (Jobs, Dienste, Verlauf).
-///
-public sealed partial class TasksPageViewModel : PageViewModel
-{
- private readonly AppHost? _host;
- private readonly ILogger? _logger;
-
- public ObservableCollection JobEntries { get; } = [];
- public ObservableCollection ServiceEntries { get; } = [];
- public ObservableCollection JobHistoryEntries { get; } = [];
-
- [ObservableProperty]
- private JobDisplayItemViewModel? _selectedJob;
-
- [ObservableProperty]
- private ServiceDisplayItemViewModel? _selectedService;
-
- [ObservableProperty]
- private string _statusText = "Bereit";
-
- [ObservableProperty]
- private bool _isRunningJob;
-
- ///
- /// Der Entwurf ersetzt die Registerkarten durch eine Segmentleiste. Fachlich ist das
- /// dasselbe: eine von drei Ansichten. Die drei Kennzeichen darunter gibt es, weil
- /// eine Bindung „Ist dieser Wert gleich Services?" in AXAML sonst jedes Mal einen
- /// Konverter mit Parameter bräuchte.
- ///
- [ObservableProperty]
- [NotifyPropertyChangedFor(nameof(IsJobsTab))]
- [NotifyPropertyChangedFor(nameof(IsServicesTab))]
- [NotifyPropertyChangedFor(nameof(IsHistoryTab))]
- private TasksTab _selectedTab = TasksTab.Jobs;
-
- public bool IsJobsTab => SelectedTab == TasksTab.Jobs;
- public bool IsServicesTab => SelectedTab == TasksTab.Services;
- public bool IsHistoryTab => SelectedTab == TasksTab.History;
-
- public event Func>? RequestAddJobDialog;
- public event Func>? RequestAddServiceDialog;
-
- public TasksPageViewModel() : this(null) { }
-
- public TasksPageViewModel(AppHost? host) : base("nav.tasks")
- {
- _host = host;
-
- if (host is not null)
- {
- _logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Tasks");
-
- host.ServiceAdmin.EnsureBuiltIns();
- RefreshJobList();
- RefreshServiceList();
- RefreshJobHistory();
-
- // Auch die Weboberflaeche legt Jobs und Dienste an. Das Ereignis kommt auf
- // dem Faden des Verursachers, deshalb ueber den Dispatcher.
- //
- // Kein Abmelden: Diese Seite lebt so lange wie die Anwendung.
- host.InstanceChanged += change => Dispatcher.UIThread.Post(() =>
- {
- switch (change.Kind)
- {
- case InstanceChangeKind.Jobs:
- case InstanceChangeKind.Agents:
- // Auch bei Agenten: Verschwindet ein Agent, verschwinden seine
- // geplanten Laeufe mit ihm.
- RefreshJobList();
- break;
-
- case InstanceChangeKind.Services:
- RefreshServiceList();
- break;
- }
- });
- }
- else
- {
- StatusText = LocalizationManager.Instance["desktop.status.design"];
- }
- }
-
- [RelayCommand]
- private void RefreshJobList()
- {
- var previous = SelectedJob?.JobId;
-
- JobEntries.Clear();
- if (_host is null) return;
-
- foreach (var job in _host.JobAdmin.List())
- JobEntries.Add(ToDisplayItem(job, _host.Runs.LastRun(job.JobId)));
-
- SelectedJob = JobEntries.FirstOrDefault(j => j.JobId == previous);
- }
-
- ///
- /// Formt einen geplanten Lauf für die Anzeige um.
- ///
- /// Die Fachschicht liefert Zeitpunkte als ; wie sie
- /// aussehen und was an ihrer Stelle steht, wenn es keinen gibt, entscheidet die
- /// Anzeige. Deshalb steht das Gedankenstrich-Zeichen hier und nicht dort.
- ///
- private static JobDisplayItemViewModel ToDisplayItem(JobSummary job, DateTime? lastRun)
- {
- var isToolJob = job.Kind == JobKind.ToolJob;
-
- var nextRun = !job.CronValid
- ? "Ungültig"
- : job.NextRunUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "—";
-
- return new JobDisplayItemViewModel(
- isToolJob ? "Tool Job" : "Agent Wakeup",
- job.JobId,
- job.AgentId,
- job.AgentName,
- isToolJob ? job.ToolName : "—",
- job.Cron,
- job.Message,
- job.RunOnStart,
- nextRun,
- lastRun?.ToString("yyyy-MM-dd HH:mm:ss") ?? "—",
- "—",
- job.Enabled
- ? "Aktiv"
- : isToolJob ? "Deaktiviert" : "Inaktiv");
- }
-
- [RelayCommand]
- private void RefreshServiceList()
- {
- var previous = SelectedService?.ServiceId;
-
- ServiceEntries.Clear();
- if (_host is null) return;
-
- foreach (var svc in _host.Instance.Services)
- {
- ServiceEntries.Add(new ServiceDisplayItemViewModel(
- svc.ServiceId,
- svc.Name,
- svc.Type,
- svc.Port,
- svc.Enabled ? "Bereit" : "Deaktiviert",
- svc.Description
- ));
- }
-
- SelectedService = ServiceEntries.FirstOrDefault(s => s.ServiceId == previous);
- }
-
- [RelayCommand]
- private void RefreshJobHistory()
- {
- JobHistoryEntries.Clear();
- if (_host is null) return;
-
- foreach (var entry in _host.Runs.History.GetAll().Take(200))
- JobHistoryEntries.Add(entry);
- }
-
- [RelayCommand]
- private async Task AddJobAsync()
- {
- if (_host is null || RequestAddJobDialog is null) return;
-
- var vm = new AddJobViewModel(_host.Instance.Agents);
- var result = await RequestAddJobDialog(vm);
- if (result is null) return;
-
- var outcome = _host.JobAdmin.Add(ToDefinition(result));
- StatusText = outcome.Message;
-
- if (outcome.Ok)
- _logger?.LogInformation("Geplanter Lauf hinzugefügt: Agent={Agent}, Cron={Cron}",
- result.AgentId, result.CronExpression);
-
- RefreshJobList();
- }
-
- /// Übersetzt das Ergebnis des Dialogs in das, womit die Fachschicht arbeitet.
- private static JobDefinition ToDefinition(AddJobResultData result)
- => new(
- result.AgentId,
- result.IsToolJob ? JobKind.ToolJob : JobKind.AgentWakeup,
- result.ToolName,
- result.JobTypeId,
- result.CronExpression,
- result.TaskMessage,
- result.RunOnStart);
-
- [RelayCommand]
- private async Task EditJobAsync()
- {
- if (_host is null || SelectedJob is null || RequestAddJobDialog is null) return;
-
- var entry = SelectedJob;
- var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
- if (agent is null) return;
-
- var isToolJob = entry.JobType == "Tool Job";
- var vm = new AddJobViewModel(_host.Instance.Agents)
- {
- SelectedAgent = agent,
- IsToolJob = isToolJob,
- SelectedToolName = isToolJob ? entry.ToolName : "FileRW",
- JobTypeId = isToolJob ? entry.TaskMessage : "PollJob",
- CronExpression = entry.CronExpression,
- TaskMessage = isToolJob ? "" : entry.TaskMessage,
- RunOnStart = entry.RunOnStart
- };
-
- var result = await RequestAddJobDialog(vm);
- if (result is null) return;
-
- StatusText = _host.JobAdmin.Update(entry.JobId, ToDefinition(result)).Message;
- RefreshJobList();
- }
-
- [RelayCommand]
- private void RemoveJob()
- {
- if (_host is null || SelectedJob is null) return;
-
- var entry = SelectedJob;
-
- StatusText = _host.JobAdmin.Remove(
- entry.AgentId,
- entry.JobId,
- entry.JobType == "Tool Job" ? JobKind.ToolJob : JobKind.AgentWakeup).Message;
-
- RefreshJobList();
- }
-
- [RelayCommand]
- private async Task RunJobNowAsync()
- {
- if (_host is null || SelectedJob is null) return;
-
- var entry = SelectedJob;
- var isToolJob = entry.JobType == "Tool Job";
-
- IsRunningJob = true;
- StatusText = $"Führe Job für '{entry.AgentName}' aus…";
-
- try
- {
- var outcome = isToolJob
- ? await _host.Runs.RunToolJobAsync(entry.AgentId, entry.JobId, CancellationToken.None)
- : await _host.Runs.RunAgentAsync(entry.AgentId, entry.TaskMessage, CancellationToken.None);
-
- StatusText = outcome.Message;
-
- if (!outcome.Ok)
- _logger?.LogWarning("Job-Ausführung fehlgeschlagen: {Grund}", outcome.Message);
-
- RefreshJobList();
- RefreshJobHistory();
- }
- finally
- {
- IsRunningJob = false;
- }
- }
-
- [RelayCommand]
- private async Task AddServiceAsync()
- {
- if (_host is null || RequestAddServiceDialog is null) return;
-
- var vm = new AddServiceViewModel();
- var result = await RequestAddServiceDialog(vm);
- if (result is null) return;
-
- StatusText = _host.ServiceAdmin.Add(new NewService(
- result.ServiceName,
- result.ServiceType,
- result.ServicePort,
- result.ServiceDescription)).Message;
-
- RefreshServiceList();
- }
-
- [RelayCommand]
- private void RemoveService()
- {
- if (_host is null || SelectedService is null) return;
-
- StatusText = _host.ServiceAdmin.Remove(SelectedService.ServiceId).Message;
- RefreshServiceList();
- }
-
- ///
- /// Schaltet einen Job an oder ab, ohne ihn zu entfernen — im Entwurf die neue
- /// Schaltfläche „Aktivieren/Deaktivieren" in der Werkzeugleiste.
- ///
- /// Gilt nur für Tool-Jobs: SchedulerConfig hat keinen solchen Schalter,
- /// ein Agent-Wakeup existiert oder existiert nicht. Ihn hier zu ergänzen wäre keine
- /// Oberflächenarbeit — der Scanner müsste den Schalter erst einmal lesen, sonst liefe
- /// ein „deaktivierter" Wakeup munter weiter.
- ///
- [RelayCommand]
- private void ToggleJobEnabled()
- {
- if (_host is null || SelectedJob is null) return;
-
- var entry = SelectedJob;
-
- StatusText = _host.JobAdmin
- .SetEnabled(entry.AgentId, entry.JobId, enabled: entry.Status != "Aktiv").Message;
-
- RefreshJobList();
-
- // Die Auswahl geht beim Neuaufbau der Liste verloren — sie wieder zu setzen
- // erspart es, für zwei Schaltvorgänge zweimal dieselbe Zeile anzuklicken.
- SelectedJob = JobEntries.FirstOrDefault(j => j.JobId == entry.JobId);
- }
-
- ///
- /// Aktiviert bzw. deaktiviert einen Dienst.
- ///
- /// Bewusst nicht „Start"/„Stop" genannt, auch wenn der Entwurf das so zeigt:
- /// Umgelegt wird das Kennzeichen Enabled in der Instanzkonfiguration. Ein
- /// laufender Dienstprozess wird davon nicht angehalten — eine Schaltfläche „Stop",
- /// nach der etwas weiterläuft, wäre schlimmer als gar keine.
- ///
- [RelayCommand]
- private void ToggleServiceEnabled()
- {
- if (_host is null || SelectedService is null) return;
-
- var id = SelectedService.ServiceId;
-
- StatusText = _host.ServiceAdmin
- .SetEnabled(id, enabled: SelectedService.Status != "Bereit").Message;
-
- RefreshServiceList();
- SelectedService = ServiceEntries.FirstOrDefault(s => s.ServiceId == id);
- }
-
- [RelayCommand]
- private void SelectTab(string? tab)
- {
- SelectedTab = tab switch
- {
- "Services" => TasksTab.Services,
- "History" => TasksTab.History,
- _ => TasksTab.Jobs
- };
- }
-
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs
deleted file mode 100644
index b6068b9..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs
+++ /dev/null
@@ -1,201 +0,0 @@
-using System.Collections.ObjectModel;
-using ClawdDotNet.App;
-using ClawdDotNet.App.Services;
-using ClawdDotNet.Core.Accounting;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-/// Ein Balken des Wochendiagramms.
-public sealed class TokenDayItem
-{
- public required string DayLabel { get; init; }
- public required string TokensLabel { get; init; }
- public required int Tokens { get; init; }
-
- ///
- /// Höhe in Bildpunkten, nicht in Prozent: Ein Balken kann sich nicht auf die Höhe
- /// seines Elternteils beziehen, ohne dass die Zeile eine feste Höhe bekommt — und
- /// eine feste Höhe im Ansichtsmodell auszurechnen ist ehrlicher als eine, die in
- /// drei Bindungen verteilt steht.
- ///
- public required double BarHeight { get; init; }
-}
-
-/// Eine Zeile der Aufschlüsselung nach Agent und Modell.
-public sealed class TokenAgentItem
-{
- public required string AgentName { get; init; }
- public required string Model { get; init; }
- public required string TokensLabel { get; init; }
- public required string CostLabel { get; init; }
- public required int Runs { get; init; }
-}
-
-///
-/// Tokennutzung der letzten sieben Tage.
-///
-/// Liest aus — dieselbe Quelle, aus der die
-/// Tagesbudgets bedient werden. Kein Diagramm-Paket: Der Entwurf zeigt sieben Balken und
-/// eine Tabelle, und dafür genügen Rechtecke mit ausgerechneter Höhe. Eine Bibliothek
-/// dafür wäre 8 MB Abhängigkeit für sieben Rechtecke.
-///
-public sealed partial class TokenUsagePageViewModel : PageViewModel
-{
- /// Höhe der Diagrammfläche in Bildpunkten (siehe Entwurf: 160 px inkl. Beschriftung).
- private const double ChartHeight = 130;
-
- /// Wie viele Läufe für die Aufschlüsselung herangezogen werden.
- private const int RecentRunLimit = 1000;
-
- private const int Days = 7;
-
- private readonly AppHost? _host;
-
- public ObservableCollection Days7 { get; } = [];
- public ObservableCollection ByAgent { get; } = [];
-
- [ObservableProperty]
- private string _totalTokens = "—";
-
- [ObservableProperty]
- private string _totalCost = "—";
-
- [ObservableProperty]
- private string _totalRuns = "—";
-
- [ObservableProperty]
- private string _statusText = "";
-
- [ObservableProperty]
- private bool _isEmpty = true;
-
- public TokenUsagePageViewModel() : this(null) { }
-
- public TokenUsagePageViewModel(AppHost? host) : base("nav.usage")
- {
- _host = host;
-
- if (host?.Usage is null)
- {
- StatusText = host is null
- ? "Design mode"
- : "No usage database for this instance.";
- return;
- }
-
- // Nicht abwarten: Der Aufbau des Fensters darf nicht auf eine Datenbankabfrage
- // warten. Die Seite füllt sich, sobald die Zahlen da sind.
- _ = RefreshAsync();
- }
-
- [RelayCommand]
- private async Task RefreshAsync()
- {
- if (_host?.Usage is not { } usage) return;
-
- try
- {
- var today = DateOnly.FromDateTime(DateTime.Now);
- var daily = new List(Days);
-
- for (var offset = Days - 1; offset >= 0; offset--)
- daily.Add(await usage.GetDailyAsync(today.AddDays(-offset), "", CancellationToken.None));
-
- var recent = await usage.GetRecentAsync(RecentRunLimit, CancellationToken.None);
-
- var since = DateTime.Now.Date.AddDays(-(Days - 1));
- var window = recent.Where(r => r.OccurredAt >= since).ToArray();
-
- BuildChart(daily);
- BuildBreakdown(window);
- BuildTotals(daily, window);
- }
- catch (Exception ex)
- {
- StatusText = $"Verbrauchsdaten konnten nicht gelesen werden: {ex.Message}";
- }
- }
-
- private void BuildChart(IReadOnlyList daily)
- {
- Days7.Clear();
-
- // Ohne Höchstwert gäbe es keinen Maßstab; ohne die Untergrenze 1 eine Division
- // durch null an dem Tag, an dem noch nichts gelaufen ist.
- var max = Math.Max(1, daily.Max(d => d.TotalTokens));
-
- foreach (var day in daily)
- {
- Days7.Add(new TokenDayItem
- {
- DayLabel = day.Date.ToString("ddd"),
- Tokens = day.TotalTokens,
- TokensLabel = day.TotalTokens >= 1000
- ? $"{day.TotalTokens / 1000d:0.#}k"
- : day.TotalTokens.ToString("N0"),
-
- // Mindestens ein Pixel, damit ein Tag mit wenigen Tokens nicht wie ein
- // Tag ohne Läufe aussieht.
- BarHeight = Math.Max(1, day.TotalTokens / (double)max * ChartHeight)
- });
- }
- }
-
- private void BuildBreakdown(IReadOnlyList runs)
- {
- ByAgent.Clear();
-
- var names = _host?.Instance.Agents
- .ToDictionary(a => a.AgentId, a => a.DisplayName, StringComparer.OrdinalIgnoreCase)
- ?? [];
-
- var groups = runs
- .GroupBy(r => (r.AgentId, r.Model))
- .OrderByDescending(g => g.Sum(r => r.PromptTokens + r.CompletionTokens));
-
- foreach (var group in groups)
- {
- var (agentId, model) = group.Key;
- var tokens = group.Sum(r => r.PromptTokens + r.CompletionTokens);
- var cost = group.Sum(r => r.CostUsd);
- var incomplete = group.Any(r => !r.CostIsKnown);
-
- ByAgent.Add(new TokenAgentItem
- {
- AgentName = names.TryGetValue(agentId, out var name) ? name : agentId,
- Model = model,
- TokensLabel = tokens.ToString("N0"),
- CostLabel = FormatCost(cost, incomplete),
- Runs = group.Count()
- });
- }
- }
-
- private void BuildTotals(IReadOnlyList daily, IReadOnlyList runs)
- {
- var tokens = daily.Sum(d => d.TotalTokens);
- var cost = daily.Sum(d => d.CostUsd);
- var incomplete = daily.Any(d => !d.CostIsComplete);
-
- TotalTokens = tokens.ToString("N0");
- TotalCost = FormatCost(cost, incomplete);
- TotalRuns = daily.Sum(d => d.RunCount).ToString("N0");
-
- IsEmpty = tokens == 0 && runs.Count == 0;
- StatusText = IsEmpty ? LocalizationManager.Instance["desktop.usage.empty"] : "";
- }
-
- ///
- /// Kosten in Euro, wie in der Statusleiste — mit demselben Kurs aus
- /// . Das Zeichen ⚠ steht für Läufe,
- /// deren Modellpreise nicht vorlagen: Die Summe ist dann zu niedrig, und das soll man
- /// ihr ansehen.
- ///
- private static string FormatCost(decimal usd, bool incomplete)
- {
- var eur = (double)usd * OpenRouterStatusService.UsdToEur;
- return incomplete ? $"{eur:F2} € ⚠" : $"{eur:F2} €";
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/ToolSettingsViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/ToolSettingsViewModel.cs
deleted file mode 100644
index 0260670..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/ToolSettingsViewModel.cs
+++ /dev/null
@@ -1,221 +0,0 @@
-using ClawdDotNet.Models;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-public sealed partial class ToolSettingsViewModel : ViewModelBase
-{
- public string ToolName { get; }
- public object? SettingsObject { get; }
-
- // FTP
- [ObservableProperty] private string _ftpHost = "";
- [ObservableProperty] private int _ftpPort = 21;
- [ObservableProperty] private string _ftpUsername = "";
- [ObservableProperty] private string _ftpPassword = "";
- [ObservableProperty] private string _ftpRootPath = "./data/";
-
- // Mail
- [ObservableProperty] private string _mailUsername = "";
- [ObservableProperty] private string _mailPassword = "";
- [ObservableProperty] private string _mailImapHost = "";
- [ObservableProperty] private int _mailImapPort = 993;
- [ObservableProperty] private string _mailSmtpHost = "";
- [ObservableProperty] private int _mailSmtpPort = 587;
- [ObservableProperty] private string _mailAllowedRecipients = "";
-
- // Database
- [ObservableProperty] private DatabaseType _dbType = DatabaseType.MySql;
- [ObservableProperty] private string _dbConnectionString = "";
- [ObservableProperty] private DatabaseAccessLevel _dbAccessLevel = DatabaseAccessLevel.ReadOnly;
- [ObservableProperty] private string _dbAllowedTables = "";
-
- // Telegram
- [ObservableProperty] private string _telegramBotToken = "";
- [ObservableProperty] private string _telegramDefaultChatId = "";
- [ObservableProperty] private string _telegramAllowedChatIds = "";
-
- // DirectAPI
- [ObservableProperty] private string _directApiDefaultProvider = "twelvedata";
- [ObservableProperty] private int _directApiCacheTtlSeconds = 60;
- [ObservableProperty] private string _directApiTwelveDataKey = "";
- [ObservableProperty] private string _directApiAlphaVantageKey = "";
-
- // WebFetch
- [ObservableProperty] private string _webFetchAllowedDomains = "";
- [ObservableProperty] private int _webFetchMaxResponseKb = 512;
- [ObservableProperty] private string _webFetchUserAgent = "ClawdDotNet-Agent/1.0";
-
- // SocialMediaManager
- [ObservableProperty] private string _smmXApiKey = "";
- [ObservableProperty] private string _smmXWatchAccounts = "";
- [ObservableProperty] private string _smmRedditWatchSubreddits = "";
- [ObservableProperty] private int _smmRedditPostLimit = 15;
- [ObservableProperty] private string _smmOpenRouterApiKey = "";
- [ObservableProperty] private string _smmSttModel = "openai/whisper-1";
- [ObservableProperty] private string _smmYoutubeChannels = "";
-
- // FileRW
- [ObservableProperty] private string _fileRwPersonalAllowedExtensions = ".txt,.json,.md,.html,.js,.css";
- [ObservableProperty] private FileRWAccessLevel _fileRwSharedAccessLevel = FileRWAccessLevel.Denied;
- [ObservableProperty] private string _fileRwSharedAllowedExtensions = ".txt,.json,.md";
- [ObservableProperty] private string _fileRwProtectedPaths = "stocks/";
-
- public List DbTypes { get; } = [DatabaseType.MySql, DatabaseType.Postgres, DatabaseType.MsSql, DatabaseType.MongoDb];
- public List DbAccessLevels { get; } = [DatabaseAccessLevel.ReadOnly, DatabaseAccessLevel.ReadWrite, DatabaseAccessLevel.Admin];
- public List FileRwAccessLevels { get; } = [FileRWAccessLevel.Denied, FileRWAccessLevel.Read, FileRWAccessLevel.ReadWrite, FileRWAccessLevel.Admin];
-
- public Dictionary? Result { get; private set; }
- public event Action? CloseRequested;
-
- public ToolSettingsViewModel() : this("FTP", new()) { }
-
- public ToolSettingsViewModel(string toolName, Dictionary config)
- {
- ToolName = toolName;
- SettingsObject = ToolSettingsFactory.CreateViewModel(toolName, config);
-
- switch (SettingsObject)
- {
- case FTPToolSettings ftp:
- FtpHost = ftp.Host;
- FtpPort = ftp.Port;
- FtpUsername = ftp.Username;
- FtpPassword = ftp.Password;
- FtpRootPath = ftp.RootPath;
- break;
-
- case MailToolSettings mail:
- MailUsername = mail.Username;
- MailPassword = mail.Password;
- MailImapHost = mail.ImapHost;
- MailImapPort = mail.ImapPort;
- MailSmtpHost = mail.SmtpHost;
- MailSmtpPort = mail.SmtpPort;
- MailAllowedRecipients = mail.AllowedRecipients;
- break;
-
- case DatabaseToolSettings db:
- DbType = db.Type;
- DbConnectionString = db.ConnectionString;
- DbAccessLevel = db.AccessLevel;
- DbAllowedTables = db.AllowedTables;
- break;
-
- case TelegramToolSettings tg:
- TelegramBotToken = tg.BotToken;
- TelegramDefaultChatId = tg.DefaultChatId;
- TelegramAllowedChatIds = tg.AllowedChatIds;
- break;
-
- case DirectAPIToolSettings dapi:
- DirectApiDefaultProvider = dapi.DefaultProvider;
- DirectApiCacheTtlSeconds = dapi.CacheTtlSeconds;
- DirectApiTwelveDataKey = dapi.TwelveDataKey;
- DirectApiAlphaVantageKey = dapi.AlphaVantageKey;
- break;
-
- case WebFetchToolSettings wf:
- WebFetchAllowedDomains = wf.AllowedDomains;
- WebFetchMaxResponseKb = wf.MaxResponseKb;
- WebFetchUserAgent = wf.UserAgent;
- break;
-
- case SocialMediaManagerToolSettings smm:
- SmmXApiKey = smm.XApiKey;
- SmmXWatchAccounts = smm.XWatchAccounts;
- SmmRedditWatchSubreddits = smm.RedditWatchSubreddits;
- SmmRedditPostLimit = smm.RedditPostLimit;
- SmmOpenRouterApiKey = smm.OpenRouterApiKey;
- SmmSttModel = smm.STTModel;
- SmmYoutubeChannels = smm.YoutubeChannels;
- break;
-
- case FileRWToolSettings frw:
- FileRwPersonalAllowedExtensions = frw.PersonalAllowedExtensions;
- FileRwSharedAccessLevel = frw.SharedAccessLevel;
- FileRwSharedAllowedExtensions = frw.SharedAllowedExtensions;
- FileRwProtectedPaths = frw.ProtectedPaths;
- break;
- }
- }
-
- [RelayCommand]
- private void Confirm()
- {
- switch (SettingsObject)
- {
- case FTPToolSettings ftp:
- ftp.Host = FtpHost;
- ftp.Port = FtpPort;
- ftp.Username = FtpUsername;
- ftp.Password = FtpPassword;
- ftp.RootPath = FtpRootPath;
- break;
-
- case MailToolSettings mail:
- mail.Username = MailUsername;
- mail.Password = MailPassword;
- mail.ImapHost = MailImapHost;
- mail.ImapPort = MailImapPort;
- mail.SmtpHost = MailSmtpHost;
- mail.SmtpPort = MailSmtpPort;
- mail.AllowedRecipients = MailAllowedRecipients;
- break;
-
- case DatabaseToolSettings db:
- db.Type = DbType;
- db.ConnectionString = DbConnectionString;
- db.AccessLevel = DbAccessLevel;
- db.AllowedTables = DbAllowedTables;
- break;
-
- case TelegramToolSettings tg:
- tg.BotToken = TelegramBotToken;
- tg.DefaultChatId = TelegramDefaultChatId;
- tg.AllowedChatIds = TelegramAllowedChatIds;
- break;
-
- case DirectAPIToolSettings dapi:
- dapi.DefaultProvider = DirectApiDefaultProvider;
- dapi.CacheTtlSeconds = DirectApiCacheTtlSeconds;
- dapi.TwelveDataKey = DirectApiTwelveDataKey;
- dapi.AlphaVantageKey = DirectApiAlphaVantageKey;
- break;
-
- case WebFetchToolSettings wf:
- wf.AllowedDomains = WebFetchAllowedDomains;
- wf.MaxResponseKb = WebFetchMaxResponseKb;
- wf.UserAgent = WebFetchUserAgent;
- break;
-
- case SocialMediaManagerToolSettings smm:
- smm.XApiKey = SmmXApiKey;
- smm.XWatchAccounts = SmmXWatchAccounts;
- smm.RedditWatchSubreddits = SmmRedditWatchSubreddits;
- smm.RedditPostLimit = SmmRedditPostLimit;
- smm.OpenRouterApiKey = SmmOpenRouterApiKey;
- smm.STTModel = SmmSttModel;
- smm.YoutubeChannels = SmmYoutubeChannels;
- break;
-
- case FileRWToolSettings frw:
- frw.PersonalAllowedExtensions = FileRwPersonalAllowedExtensions;
- frw.SharedAccessLevel = FileRwSharedAccessLevel;
- frw.SharedAllowedExtensions = FileRwSharedAllowedExtensions;
- frw.ProtectedPaths = FileRwProtectedPaths;
- break;
- }
-
- Result = ToolSettingsFactory.ToConfig(ToolName, SettingsObject) ?? new();
- CloseRequested?.Invoke();
- }
-
- [RelayCommand]
- private void Cancel()
- {
- Result = null;
- CloseRequested?.Invoke();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs b/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs
deleted file mode 100644
index 0b15a99..0000000
--- a/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using ClawdDotNet.Desktop.Services;
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace ClawdDotNet.Desktop.ViewModels;
-
-///
-/// Grundlage aller Ansichtsmodelle.
-///
-/// aus dem CommunityToolkit bringt
-/// INotifyPropertyChanged samt Quelltextgenerator mit — aus
-/// [ObservableProperty] private string _name; wird die vollständige Eigenschaft
-/// mit Benachrichtigung. Das spart gegenüber der WinForms-Fassung, in der die
-/// Oberfläche bei jeder Änderung von Hand nachgezogen wurde
-/// (38 Stellen mit Invoke/BeginInvoke).
-///
-/// macht die Sprachpakete in jeder Ansicht als
-/// {Binding L[schluessel]} erreichbar.
-///
-public abstract class ViewModelBase : ObservableObject
-{
- public LocalizationManager L => LocalizationManager.Instance;
-}
diff --git a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml b/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml
deleted file mode 100644
index 4704a23..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml.cs
deleted file mode 100644
index a538132..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class AddAgentWindow : Window
-{
- public AddAgentWindow()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is AddAgentViewModel vm)
- {
- vm.CloseRequested += () => Close();
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml b/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml
deleted file mode 100644
index fa04d4a..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml.cs
deleted file mode 100644
index 746812d..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddJobWindow.axaml.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class AddJobWindow : Window
-{
- public AddJobWindow()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is AddJobViewModel vm)
- {
- vm.CloseRequested += () => Close();
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml b/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml
deleted file mode 100644
index e6b9f08..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml.cs
deleted file mode 100644
index ae9e848..0000000
--- a/src/ClawdDotNet.Desktop/Views/AddServiceWindow.axaml.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class AddServiceWindow : Window
-{
- public AddServiceWindow()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is AddServiceViewModel vm)
- {
- vm.CloseRequested += () => Close();
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml b/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml
deleted file mode 100644
index 87cea37..0000000
--- a/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml.cs
deleted file mode 100644
index ffd7add..0000000
--- a/src/ClawdDotNet.Desktop/Views/AgentChatsPageView.axaml.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Avalonia.Controls;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class AgentChatsPageView : UserControl
-{
- public AgentChatsPageView() => InitializeComponent();
-}
diff --git a/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml b/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml
deleted file mode 100644
index 2dd8cca..0000000
--- a/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml
+++ /dev/null
@@ -1,293 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml.cs
deleted file mode 100644
index 2cc01f3..0000000
--- a/src/ClawdDotNet.Desktop/Views/AgentsPageView.axaml.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class AgentsPageView : UserControl
-{
- private AgentsPageViewModel? _viewModel;
-
- public AgentsPageView()
- {
- InitializeComponent();
-
- // Ein Zug am Teiler ist erst zu Ende, wenn die Maus losgelassen wird. Waehrend
- // des Ziehens zu speichern hiesse, fuer eine Bewegung ein paar Dutzend Mal die
- // Einstellungsdatei zu schreiben.
- TopSplitter.DragCompleted += (_, _) => SaveSplits();
- BottomSplitter.DragCompleted += (_, _) => SaveSplits();
- VerticalSplitter.DragCompleted += (_, _) => SaveSplits();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
-
- if (DataContext is not AgentsPageViewModel vm) return;
-
- _viewModel = vm;
- vm.RequestAddAgentDialog += OnRequestAddAgentDialog;
- vm.RequestTextEditorDialog += OnRequestTextEditorDialog;
-
- ApplySavedSplits(vm);
- }
-
- ///
- /// Setzt die gespeicherten Teilerstellungen.
- ///
- /// Von Hand und nicht per Bindung: und
- /// liegen nicht im Sichtbarkeitsbaum und erben deshalb
- /// keinen DataContext. Eine Bindung auf ihre Breite bliebe ohne Wirkung — und zwar
- /// ohne Fehlermeldung.
- ///
- private void ApplySavedSplits(AgentsPageViewModel vm)
- {
- SetRatio(TopGrid.ColumnDefinitions[0], TopGrid.ColumnDefinitions[2], vm.SplitTopList);
- SetRatio(BottomGrid.ColumnDefinitions[0], BottomGrid.ColumnDefinitions[2], vm.SplitBottomList);
-
- OuterGrid.RowDefinitions[0].Height = new GridLength(vm.SplitVertical, GridUnitType.Star);
- OuterGrid.RowDefinitions[2].Height = new GridLength(1 - vm.SplitVertical, GridUnitType.Star);
- }
-
- private static void SetRatio(ColumnDefinition first, ColumnDefinition second, double ratio)
- {
- first.Width = new GridLength(ratio, GridUnitType.Star);
- second.Width = new GridLength(1 - ratio, GridUnitType.Star);
- }
-
- private void SaveSplits()
- {
- if (_viewModel is null) return;
-
- _viewModel.SaveSplits(
- RowRatio(OuterGrid.RowDefinitions[0], OuterGrid.RowDefinitions[2]),
- ColumnRatio(TopGrid.ColumnDefinitions[0], TopGrid.ColumnDefinitions[2]),
- ColumnRatio(BottomGrid.ColumnDefinitions[0], BottomGrid.ColumnDefinitions[2]));
- }
-
- ///
- /// Das Verhaeltnis wird aus den tatsaechlichen Pixelbreiten gebildet, nicht aus den
- /// Sternwerten: Der GridSplitter setzt beim Ziehen mitunter absolute Breiten, und
- /// dann stuenden in den Sternwerten noch die Zahlen von vor dem Zug.
- ///
- private static double ColumnRatio(ColumnDefinition first, ColumnDefinition second)
- => Ratio(first.ActualWidth, second.ActualWidth);
-
- private static double RowRatio(RowDefinition first, RowDefinition second)
- => Ratio(first.ActualHeight, second.ActualHeight);
-
- private static double Ratio(double first, double second)
- {
- var total = first + second;
- return total > 0 ? first / total : 0.5;
- }
-
- private async Task OnRequestAddAgentDialog(AddAgentViewModel vm)
- {
- if (TopLevel.GetTopLevel(this) is not Window window) return null;
-
- var dialog = new AddAgentWindow { DataContext = vm };
- await dialog.ShowDialog(window);
- return vm.Result;
- }
-
- private async Task OnRequestTextEditorDialog(string title, string text)
- {
- if (TopLevel.GetTopLevel(this) is not Window window) return null;
-
- return await TextEditorWindow.ShowAsync(window, title, text);
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml b/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml
deleted file mode 100644
index 4ded51f..0000000
--- a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml.cs
deleted file mode 100644
index dbc80ef..0000000
--- a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Platform.Storage;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class BackupPageView : UserControl
-{
- public BackupPageView()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is BackupPageViewModel vm)
- {
- vm.RequestRestoreDialog += OnRequestRestoreDialog;
- }
- }
-
- private async Task OnRequestRestoreDialog(RestoreBackupViewModel vm)
- {
- var topLevel = TopLevel.GetTopLevel(this);
- if (topLevel is Window window)
- {
- var dialog = new RestoreBackupWindow { DataContext = vm };
- await dialog.ShowDialog(window);
- return vm.Result;
- }
- return null;
- }
-
- private async void OnBrowseTargetFolder(object? sender, RoutedEventArgs e)
- {
- if (DataContext is not BackupPageViewModel vm) return;
-
- var topLevel = TopLevel.GetTopLevel(this);
- if (topLevel?.StorageProvider is null) return;
-
- var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
- {
- Title = "Zielordner für Sicherungen wählen",
- AllowMultiple = false
- });
-
- if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } path)
- {
- vm.TargetFolder = path;
- vm.SaveSettings();
- vm.RefreshBackupListCommand.Execute(null);
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml b/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml
deleted file mode 100644
index 925ec1d..0000000
--- a/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml.cs
deleted file mode 100644
index a480aa1..0000000
--- a/src/ClawdDotNet.Desktop/Views/ChatPageView.axaml.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Input;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class ChatPageView : UserControl
-{
- public ChatPageView()
- {
- InitializeComponent();
- }
-
- private void OnInputKeyDown(object? sender, KeyEventArgs e)
- {
- if (e.Key == Key.Enter && e.KeyModifiers.HasFlag(KeyModifiers.Control))
- {
- if (DataContext is ChatPageViewModel vm && vm.SendMessageCommand.CanExecute(null))
- {
- vm.SendMessageCommand.Execute(null);
- e.Handled = true;
- }
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml b/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml
deleted file mode 100644
index 3fabb9f..0000000
--- a/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml.cs
deleted file mode 100644
index 21db0ba..0000000
--- a/src/ClawdDotNet.Desktop/Views/InfoPageView.axaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.Controls;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class InfoPageView : UserControl
-{
- public InfoPageView()
- {
- InitializeComponent();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml b/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml
deleted file mode 100644
index 5768ab3..0000000
--- a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml.cs
deleted file mode 100644
index 870b08d..0000000
--- a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Input;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class InstancePickerWindow : Window
-{
- public InstancePickerWindow()
- {
- InitializeComponent();
-
- // Doppelklick auf eine Zeile bestaetigt — wie in der WinForms-Fassung.
- //
- // Der Behandler haengt an der Liste und nicht mehr am Fenster: Am Fenster loeste
- // auch ein Doppelklick ins Namensfeld die Auswahl aus, was seit dem Umbau
- // deutlich leichter passiert, weil das Feld gross und mittig steht.
- InstanceList.AddHandler(DoubleTappedEvent, OnRowDoubleTapped, handledEventsToo: false);
- }
-
- private void OnRowDoubleTapped(object? sender, TappedEventArgs e)
- {
- if (DataContext is not InstancePickerViewModel vm) return;
-
- // Getroffen wird irgendein Element in der Zeile; der Datenkontext ist derselbe.
- if ((e.Source as Control)?.DataContext is not InstanceChoiceViewModel choice) return;
-
- vm.ActivateCommand.Execute(choice);
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml b/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml
deleted file mode 100644
index f9d7537..0000000
--- a/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml.cs
deleted file mode 100644
index c4ceb2a..0000000
--- a/src/ClawdDotNet.Desktop/Views/LicenseWindow.axaml.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class LicenseWindow : Window
-{
- public LicenseWindow()
- {
- InitializeComponent();
-
- // Wer das Fenster ueber das Kreuz schliesst, hat abgebrochen. Ohne das bliebe
- // der wartende Task fuer immer offen und der Start haenge.
- Closed += (_, _) => (DataContext as LicenseViewModel)?.CancelIfPending();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/LogPageView.axaml b/src/ClawdDotNet.Desktop/Views/LogPageView.axaml
deleted file mode 100644
index 850f02b..0000000
--- a/src/ClawdDotNet.Desktop/Views/LogPageView.axaml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/LogPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/LogPageView.axaml.cs
deleted file mode 100644
index bbf7971..0000000
--- a/src/ClawdDotNet.Desktop/Views/LogPageView.axaml.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class LogPageView : UserControl
-{
- public LogPageView()
- {
- InitializeComponent();
-
- // Mitlaufen: Nach jeder neuen Zeile ans Ende springen — aber nur, solange der
- // Haken gesetzt ist. Das gehoert in die Ansicht, weil nur sie den ScrollViewer
- // kennt; das Ansichtsmodell haelt lediglich den Schalter.
- Scroller.ScrollChanged += (_, _) =>
- {
- if (DataContext is LogPageViewModel { FollowTail: true })
- Scroller.ScrollToEnd();
- };
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml b/src/ClawdDotNet.Desktop/Views/MainWindow.axaml
deleted file mode 100644
index 8ed1080..0000000
--- a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml
+++ /dev/null
@@ -1,231 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/MainWindow.axaml.cs
deleted file mode 100644
index a1c654c..0000000
--- a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Threading;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class MainWindow : Window
-{
- private bool _confirmedClose;
-
- public MainWindow()
- {
- InitializeComponent();
- Closing += OnClosing;
- }
-
- ///
- /// Schließt ohne Rückfrage — für eine Beendigung, die nicht von diesem Fenster ausgeht.
- ///
- /// Warum das nötig ist. Der Bestätigungsdialog schützt vor dem
- /// versehentlichen Klick eines Menschen. Bittet dagegen der Instanzmanager über den
- /// Socket, steht niemand davor: Der Dialog bliebe offen, die zehn Sekunden liefen ab,
- /// und niemand klickte. Der Manager wartete seine Frist ab und schösse den Prozess
- /// dann ab — der Dialog, der ein hartes Ende verhindern soll, hätte es garantiert.
- ///
- public void BeendenOhneRueckfrage()
- {
- _confirmedClose = true;
- Close();
- }
-
- private async void OnClosing(object? sender, WindowClosingEventArgs e)
- {
- if (_confirmedClose) return;
-
- // Schließen zunächst abbrechen, um Bestätigungsdialog anzuzeigen
- e.Cancel = true;
-
- var confirmed = await ShowExitConfirmationAsync(this);
- if (confirmed)
- {
- _confirmedClose = true;
- Close();
- }
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
-
- // Der Instanzwechsel haengt nicht mehr hier, sondern in App: Er braucht den
- // Aufbau und die Lebensdauer der Anwendung, und beides kennt das Fenster nicht.
- // Vorher wurde hier ein InstancePickerWindow ganz ohne DataContext geoeffnet —
- // ein leerer Dialog, dessen Auswahl niemand ausgewertet hat.
- if (DataContext is MainWindowViewModel vm)
- vm.RequestExit += Close;
- }
-
- internal static async Task ShowExitConfirmationAsync(Window? owner)
- {
- var completion = new TaskCompletionSource();
- var remainingSeconds = 10;
-
- var cancelBtn = new Button
- {
- Content = "Abbrechen (Instanz läuft weiter)",
- IsCancel = true
- };
-
- var exitBtn = new Button
- {
- Content = $"Instanz beenden ({remainingSeconds}s)",
- IsEnabled = false
- };
- exitBtn.Classes.Add("danger");
-
- var countdownText = new TextBlock
- {
- Text = $"Bestätigung gesperrt: Noch {remainingSeconds} Sekunden…",
- Classes = { "caption" }
- };
-
- var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
- timer.Tick += (_, _) =>
- {
- remainingSeconds--;
- if (remainingSeconds > 0)
- {
- countdownText.Text = $"Bestätigung gesperrt: Noch {remainingSeconds} Sekunden…";
- exitBtn.Content = $"Instanz beenden ({remainingSeconds}s)";
- }
- else
- {
- timer.Stop();
- exitBtn.IsEnabled = true;
- exitBtn.Content = "Instanz jetzt endgültig beenden";
- countdownText.Text = "Sperre aufgehoben — Bestätigung jetzt möglich.";
- }
- };
-
- var window = new Window
- {
- Title = "ClawdDotNet – Instanz beenden",
- Width = 500,
- SizeToContent = SizeToContent.Height,
- CanResize = false,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Icon = owner?.Icon,
- Content = new StackPanel
- {
- Margin = new Thickness(20),
- Spacing = 16,
- Children =
- {
- new TextBlock
- {
- Text = "Achtung: Produktivinstanz beenden",
- Classes = { "h1" }
- },
- new TextBlock
- {
- Text = "ClawdDotNet-Instanzen sind auf dauerhaften Dauerbetrieb ausgelegt. "
- + "Das Beenden stoppt alle aktiven Agenten, Automationen, Web-Zugriffe und Hintergrundaufgaben dieser Instanz geordnet.",
- TextWrapping = Avalonia.Media.TextWrapping.Wrap
- },
- countdownText,
- new StackPanel
- {
- Orientation = Avalonia.Layout.Orientation.Horizontal,
- Spacing = 10,
- HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
- Children = { cancelBtn, exitBtn }
- }
- }
- }
- };
-
- cancelBtn.Click += (_, _) =>
- {
- timer.Stop();
- completion.TrySetResult(false);
- window.Close();
- };
-
- exitBtn.Click += (_, _) =>
- {
- timer.Stop();
- completion.TrySetResult(true);
- window.Close();
- };
-
- window.Closed += (_, _) =>
- {
- timer.Stop();
- completion.TrySetResult(false);
- };
-
- timer.Start();
-
- if (owner is not null)
- await window.ShowDialog(owner);
- else
- window.Show();
-
- return await completion.Task;
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml b/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml
deleted file mode 100644
index 4a22dfb..0000000
--- a/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml.cs
deleted file mode 100644
index 14adbfa..0000000
--- a/src/ClawdDotNet.Desktop/Views/RestoreBackupWindow.axaml.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Platform.Storage;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class RestoreBackupWindow : Window
-{
- public RestoreBackupWindow()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is RestoreBackupViewModel vm)
- {
- vm.CloseRequested += () => Close();
- }
- }
-
- private async void OnBrowseFolder(object? sender, RoutedEventArgs e)
- {
- if (DataContext is not RestoreBackupViewModel vm) return;
-
- var storage = StorageProvider;
- if (storage is null) return;
-
- var folders = await storage.OpenFolderPickerAsync(new FolderPickerOpenOptions
- {
- Title = "Zielordner für Wiederherstellung wählen",
- AllowMultiple = false
- });
-
- if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } path)
- {
- vm.TargetDirectory = path;
- }
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml b/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml
deleted file mode 100644
index 58c5ebf..0000000
--- a/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml.cs
deleted file mode 100644
index 790d578..0000000
--- a/src/ClawdDotNet.Desktop/Views/SettingsPageView.axaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.Controls;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class SettingsPageView : UserControl
-{
- public SettingsPageView()
- {
- InitializeComponent();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml b/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml
deleted file mode 100644
index 9ae3046..0000000
--- a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml
+++ /dev/null
@@ -1,257 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml.cs
deleted file mode 100644
index fae5b62..0000000
--- a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Avalonia.Controls;
-using ClawdDotNet.Desktop.ViewModels;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class TasksPageView : UserControl
-{
- public TasksPageView()
- {
- InitializeComponent();
- }
-
- protected override void OnDataContextChanged(EventArgs e)
- {
- base.OnDataContextChanged(e);
- if (DataContext is TasksPageViewModel vm)
- {
- vm.RequestAddJobDialog += OnRequestAddJobDialog;
- vm.RequestAddServiceDialog += OnRequestAddServiceDialog;
- }
- }
-
- private async Task OnRequestAddJobDialog(AddJobViewModel vm)
- {
- var topLevel = TopLevel.GetTopLevel(this);
- if (topLevel is Window window)
- {
- var dialog = new AddJobWindow { DataContext = vm };
- await dialog.ShowDialog(window);
- return vm.Result;
- }
- return null;
- }
-
- private async Task OnRequestAddServiceDialog(AddServiceViewModel vm)
- {
- var topLevel = TopLevel.GetTopLevel(this);
- if (topLevel is Window window)
- {
- var dialog = new AddServiceWindow { DataContext = vm };
- await dialog.ShowDialog(window);
- return vm.Result;
- }
- return null;
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml b/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml
deleted file mode 100644
index c97b157..0000000
--- a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml.cs b/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml.cs
deleted file mode 100644
index a41f7ef..0000000
--- a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-
-namespace ClawdDotNet.Desktop.Views;
-
-///
-/// Kleiner Editor für die langen Textfelder eines Agenten (Identity, Soul).
-///
-/// Ohne eigenes Ansichtsmodell: Der Dialog hat einen Titel, ein Textfeld und zwei
-/// Schaltflächen. Ein Ansichtsmodell dafür wäre eine Datei mehr, ohne dass irgendetwas
-/// prüfbar würde.
-///
-public partial class TextEditorWindow : Window
-{
- private string? _result;
-
- public TextEditorWindow()
- {
- InitializeComponent();
- Title = ClawdDotNet.Desktop.Services.LocalizationManager.Instance["desktop.dialog.textEditorTitle"] ?? "Edit Text";
- CancelButton.Content = ClawdDotNet.Desktop.Services.LocalizationManager.Instance["common.cancel"] ?? "Cancel";
- SaveButton.Content = ClawdDotNet.Desktop.Services.LocalizationManager.Instance["common.save"] ?? "Save";
- }
-
- ///
- /// Zeigt den Editor und liefert den bearbeiteten Text — oder null, wenn
- /// abgebrochen wurde. Das Schließen über das Fensterkreuz gilt als Abbruch: Sonst
- /// wäre ein weggeklickter Dialog dasselbe wie ein bestätigter.
- ///
- public static async Task ShowAsync(Window owner, string title, string text)
- {
- var window = new TextEditorWindow { Title = title };
- window.TitleText.Text = title;
- window.Editor.Text = text;
-
- await window.ShowDialog(owner);
- return window._result;
- }
-
- private void OnSave(object? sender, RoutedEventArgs e)
- {
- _result = Editor.Text ?? "";
- Close();
- }
-
- private void OnCancel(object? sender, RoutedEventArgs e)
- {
- _result = null;
- Close();
- }
-}
diff --git a/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml b/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml
deleted file mode 100644
index 3a28eeb..0000000
--- a/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml.cs b/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml.cs
deleted file mode 100644
index d3d57b3..0000000
--- a/src/ClawdDotNet.Desktop/Views/TokenUsagePageView.axaml.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Avalonia.Controls;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class TokenUsagePageView : UserControl
-{
- public TokenUsagePageView() => InitializeComponent();
-}
diff --git a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml b/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml
deleted file mode 100644
index f05bd54..0000000
--- a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml.cs b/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml.cs
deleted file mode 100644
index 99ec1b7..0000000
--- a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Avalonia.Controls;
-
-namespace ClawdDotNet.Desktop.Views;
-
-public partial class ToolSettingsView : UserControl
-{
- public ToolSettingsView() => InitializeComponent();
-}
diff --git a/src/ClawdDotNet.Desktop/i18n/de.json b/src/ClawdDotNet.Desktop/i18n/de.json
deleted file mode 100644
index ca9f13f..0000000
--- a/src/ClawdDotNet.Desktop/i18n/de.json
+++ /dev/null
@@ -1,341 +0,0 @@
-{
- "desktop.mainWindow.title": "ClawdDotNet",
- "desktop.mainWindow.sidebarTooltip": "Seitenleiste ein-/ausklappen",
- "desktop.mainWindow.switchInstance": "Instanz wechseln",
- "desktop.mainWindow.instance": "INSTANZ",
- "desktop.mainWindow.themeLight": "Hellmodus",
- "desktop.mainWindow.themeDark": "Dunkelmodus",
- "desktop.mainWindow.themeToggle": "Farbschema umschalten",
- "desktop.mainWindow.openInstanceFolder": "Instanz-Ordner öffnen",
- "desktop.mainWindow.instanceManager": "Instanzmanager",
- "desktop.mainWindow.exit": "Beenden",
- "desktop.mainWindow.instanceLabel": "Instanz: {0}",
- "desktop.mainWindow.apiStatus": "API {0}",
- "desktop.mainWindow.credits": "Credits: ${0:F2} von ${1:F2}",
- "desktop.mainWindow.creditsNone": "Credits: —",
- "desktop.mainWindow.dismiss": "Ausblenden",
-
- "desktop.nav.work": "Arbeiten",
- "desktop.nav.analytics": "Analyse",
- "desktop.nav.system": "System",
-
- "desktop.status.unknown": "unbekannt",
- "desktop.status.design": "Entwurf",
- "desktop.status.noApiKey": "kein API-Key",
- "desktop.status.noUsageData": "keine Nutzungsdaten",
- "desktop.status.ready": "Bereit",
- "desktop.status.reachable": "OK",
- "desktop.status.unreachable": "nicht erreichbar",
-
- "desktop.chat.agent": "Agent:",
- "desktop.chat.clearHistory": "Verlauf leeren",
- "desktop.chat.abort": "Abbrechen",
- "desktop.chat.inputPlaceholder": "Nachricht eingeben… (Enter zum Senden, Shift+Enter für neue Zeile)",
- "desktop.chat.send": "Senden",
- "desktop.chat.statusReady": "Bereit",
- "desktop.chat.statusSending": "Sende…",
- "desktop.chat.statusDesign": "Entwurfsmodus",
- "desktop.chat.noApiKey": "OpenRouter API-Key ist nicht konfiguriert. Bitte in den Einstellungen hinterlegen.",
- "desktop.chat.noAgent": "Bitte einen Agenten auswählen.",
- "desktop.chat.emptyMessage": "Bitte eine Nachricht eingeben.",
-
- "desktop.agents.addAgent": "Agent",
- "desktop.agents.removeAgent": "Agent",
- "desktop.agents.runNow": "Jetzt ausf.",
- "desktop.agents.identity": "Identity",
- "desktop.agents.soul": "Soul",
- "desktop.agents.folder": "Ordner",
- "desktop.agents.save": "Speichern",
- "desktop.agents.thDisplayName": "ANZEIGENAME",
- "desktop.agents.thModel": "MODELL",
- "desktop.agents.thSummary": "KURZINFO",
- "desktop.agents.kickerGeneralModel": "ALLGEMEIN & MODELL",
- "desktop.agents.agentId": "Agent-ID",
- "desktop.agents.displayName": "Anzeigename",
- "desktop.agents.model": "Modell",
- "desktop.agents.promptCaching": "Prompt Caching",
- "desktop.agents.roleDescription": "Rolle / Beschreibung",
- "desktop.agents.systemPrompt": "System-Prompt (zusätzliche Anweisungen)",
- "desktop.agents.kickerLimits": "GRENZEN",
- "desktop.agents.limitsTitle": "Schutzgrenzen (LoopGuard & Budget)",
- "desktop.agents.maxSteps": "Max. Schritte pro Run",
- "desktop.agents.timeoutSeconds": "Timeout (Sekunden)",
- "desktop.agents.maxContextTokens": "Max. Kontext-Tokens",
- "desktop.agents.maxToolResultChars": "Max. Tool-Ergebnis (Zeichen)",
- "desktop.agents.dailyCost": "Tagesbudget ($ USD)",
- "desktop.agents.dailyTokens": "Tagesbudget (Tokens)",
- "desktop.agents.whenExhausted": "Wenn das Budget erschöpft ist (wiederkehrende Aufgaben)",
- "desktop.agents.whenExhaustedHint": "expire: Der verpasste Lauf verfällt und wird an der Aufgabe vermerkt. catchUpOnce: Alle verpassten Läufe werden am nächsten Tag durch einen Lauf nachgeholt, nach der regulären Arbeit des Tages. Einmalige Aufgaben werden immer auf den nächsten Tag mitgenommen.",
- "desktop.agents.allowedToolsFor": "ZUGELASSENE WERKZEUGE FÜR {0}",
- "desktop.agents.thTool": "WERKZEUG",
- "desktop.agents.thDescription": "BESCHREIBUNG",
- "desktop.agents.toolSettingsKicker": "WERKZEUG-EINSTELLUNGEN",
- "desktop.agents.apply": "Übernehmen",
- "desktop.agents.saved": "Agenten erfolgreich gespeichert.",
-
- "desktop.tasks.tabJobs": "Geplante Jobs",
- "desktop.tasks.tabServices": "Dienste",
- "desktop.tasks.tabHistory": "Verlauf",
- "desktop.tasks.addJob": "Job hinzufügen",
- "desktop.tasks.editJob": "Bearbeiten",
- "desktop.tasks.removeJob": "Entfernen",
- "desktop.tasks.toggleJob": "Aktivieren/Deaktivieren",
- "desktop.tasks.runJobNow": "Jetzt ausführen",
- "desktop.tasks.refresh": "Aktualisieren",
- "desktop.tasks.thType": "TYP",
- "desktop.tasks.thAgent": "AGENT",
- "desktop.tasks.thTask": "TOOL / NACHRICHT",
- "desktop.tasks.thCron": "CRON",
- "desktop.tasks.thNextRun": "NÄCHSTER LAUF",
- "desktop.tasks.thLastRun": "LETZTER LAUF",
- "desktop.tasks.thStatus": "STATUS",
-
- "desktop.services.addService": "Dienst hinzufügen",
- "desktop.services.removeService": "Entfernen",
- "desktop.services.toggleService": "Aktivieren/Deaktivieren",
- "desktop.services.thName": "DIENSTNAME",
- "desktop.services.thType": "TYP",
- "desktop.services.thPort": "PORT",
- "desktop.services.thStatus": "STATUS",
- "desktop.services.thDescription": "BESCHREIBUNG",
-
- "desktop.history.refresh": "Verlauf aktualisieren",
- "desktop.history.thTime": "ZEITPUNKT",
- "desktop.history.thJob": "JOB",
- "desktop.history.thAgent": "AGENT",
- "desktop.history.thDescription": "BESCHREIBUNG",
- "desktop.history.thStatus": "STATUS",
- "desktop.history.thDetails": "DETAILS",
-
- "desktop.usage.kickerTokens7d": "TOKENS (7 TAGE)",
- "desktop.usage.kickerCost7d": "KOSTEN (7 TAGE)",
- "desktop.usage.kickerRuns7d": "RUNS (7 TAGE)",
- "desktop.usage.refresh": "Aktualisieren",
- "desktop.usage.tokensPerDay": "Tokens pro Tag",
- "desktop.usage.breakdownTitle": "Aufschlüsselung nach Agent & Modell",
- "desktop.usage.thAgent": "AGENT",
- "desktop.usage.thModel": "MODELL",
- "desktop.usage.thTokens": "TOKENS",
- "desktop.usage.thCost": "KOSTEN",
- "desktop.usage.thRuns": "RUNS",
- "desktop.usage.empty": "Noch keine Tokennutzung aufgezeichnet.",
-
- "desktop.agentChats.agentA": "Agent A:",
- "desktop.agentChats.agentB": "Agent B:",
- "desktop.agentChats.empty": "Noch keine Nachrichten zwischen diesen Agenten aufgezeichnet.",
- "desktop.agentChats.needTwo": "Für einen Verlauf braucht es mindestens zwei Agenten in dieser Instanz.",
-
- "desktop.backup.kickerManual": "MANUELL",
- "desktop.backup.titleCreate": "Sicherung erstellen",
- "desktop.backup.targetFolder": "Zielordner",
- "desktop.backup.targetFolderPlaceholder": "Zielordner für Sicherungen…",
- "desktop.backup.browse": "Durchsuchen…",
- "desktop.backup.credentials": "Zugangsdaten",
- "desktop.backup.credentialsExclude": "nicht mitsichern",
- "desktop.backup.credentialsProtect": "mit Passphrase schützen",
- "desktop.backup.passphrase": "Passphrase eingeben",
- "desktop.backup.passphraseRepeat": "Passphrase wiederholen",
- "desktop.backup.scope": "Umfang",
- "desktop.backup.scopeChat": "Chatverläufe",
- "desktop.backup.scopeLogs": "Protokolle",
- "desktop.backup.createNow": "Sicherung jetzt erstellen",
- "desktop.backup.kickerAuto": "AUTOMATIK",
- "desktop.backup.titleAuto": "Automatische Sicherung",
- "desktop.backup.autoDaily": "täglich sichern um",
- "desktop.backup.time": "Uhrzeit",
- "desktop.backup.keep": "Aufbewahren",
- "desktop.backup.autoHint": "Die Automatik sichert ohne Zugangsdaten — eine gespeicherte Passphrase wäre wirkungslos.",
- "desktop.backup.kickerExisting": "BESTAND",
- "desktop.backup.titleExisting": "Vorhandene Sicherungen",
- "desktop.backup.refresh": "Aktualisieren",
- "desktop.backup.openFolder": "Ordner öffnen",
- "desktop.backup.delete": "Löschen",
- "desktop.backup.restore": "Wiederherstellen…",
- "desktop.backup.thFilename": "DATEINAME",
- "desktop.backup.thCreatedAt": "ERSTELLT AM",
- "desktop.backup.thInstance": "INSTANZ",
- "desktop.backup.thSize": "GRÖSSE",
- "desktop.backup.thSecrets": "ZUGANGSDATEN",
-
- "desktop.logs.module": "Modul:",
- "desktop.logs.level": "Stufe:",
- "desktop.logs.refresh": "Aktualisieren",
- "desktop.logs.clear": "Leeren",
- "desktop.logs.openFolder": "Ordner öffnen",
- "desktop.logs.follow": "Mitlaufen",
-
- "desktop.settings.discard": "Verwerfen",
- "desktop.settings.save": "Speichern",
- "desktop.settings.instance": "INSTANZ",
- "desktop.settings.instanceName": "Instanz-Name",
- "desktop.settings.apiKey": "OpenRouter API-Key",
- "desktop.settings.autostart": "Autostart durch Manager",
- "desktop.settings.autostartHint": "Der Instanzmanager startet diese Instanz, wenn er hochkommt, und wieder, wenn sie abstürzt. Nach mehreren Neustarts in kurzer Folge setzt er aus und meldet es.",
- "desktop.settings.logDir": "Protokoll-Verzeichnis",
- "desktop.settings.logDirHint": "Gilt nur für diese Instanz. Leer heißt: Unterordner Logs im Instanzverzeichnis.",
- "desktop.settings.dailyCost": "Tagesbudget ($ USD)",
- "desktop.settings.dailyTokens": "Tagesbudget (Tokens)",
- "desktop.settings.general": "ALLGEMEIN",
- "desktop.settings.instancesDir": "Instanzen-Verzeichnis",
- "desktop.settings.minLogLevel": "Minimaler Log-Level",
- "desktop.settings.openRouterUrl": "OpenRouter Base-URL",
- "desktop.settings.language": "Sprache",
- "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LIZENZ",
- "desktop.settings.dcUrl": "Server-URL",
- "desktop.settings.dcToken": "Master-Token",
- "desktop.settings.dcEnv": "Umgebung",
- "desktop.settings.updateChannel": "Update-Kanal",
- "desktop.settings.licenseKey": "Lizenzschlüssel",
- "desktop.settings.errorReporting": "Fehler automatisch an Deploymentcenter melden",
- "desktop.settings.updateCheck": "Beim Start automatisch auf Updates prüfen",
- "desktop.settings.saved": "Einstellungen und Instanz-Konfiguration gespeichert.",
- "desktop.settings.savedLogRestart": "Gespeichert. Das Protokoll-Verzeichnis gilt ab dem nächsten Start.",
-
- "desktop.info.kickerApp": "ANWENDUNG",
- "desktop.info.build": "Build",
- "desktop.info.version": "Version",
- "desktop.info.buildSummary": "Build-Zusammenfassung",
- "desktop.info.kickerInstance": "INSTANZ",
- "desktop.info.currentInstance": "Aktuelle Instanz",
- "desktop.info.openFolder": "Instanz-Ordner öffnen",
- "desktop.info.name": "Name",
- "desktop.info.id": "ID",
- "desktop.info.path": "Pfad",
- "desktop.info.logDir": "Log-Ordner",
- "desktop.info.openRouter": "OpenRouter",
- "desktop.info.kickerModules": "MODULE",
- "desktop.info.assemblies": "Baugruppen",
- "desktop.info.thModule": "MODUL",
- "desktop.info.thBuild": "BUILD",
- "desktop.info.thBuildDate": "BUILD-DATUM",
- "desktop.info.thChanges": "ÄNDERUNGEN",
-
- "desktop.dialog.addAgent": "Neuen Agenten anlegen",
- "desktop.dialog.agentId": "Agent-ID (eindeutiger Schlüssel):",
- "desktop.dialog.agentDisplayName": "Anzeigename:",
- "desktop.dialog.defaultModel": "Standard-Modell:",
- "desktop.dialog.roleDescription": "Beschreibung / Rolle:",
- "desktop.dialog.addJobTitle": "Job hinzufügen / bearbeiten",
- "desktop.dialog.jobType": "Job-Typ:",
- "desktop.dialog.agentWakeup": "Agent Wakeup (Periodische Ausführung)",
- "desktop.dialog.toolJob": "Tool Job (Werkzeug-spezifischer Job)",
- "desktop.dialog.jobTypeId": "Job-Typ-ID / Name:",
- "desktop.dialog.taskMessage": "Aufgabenbeschreibung / Nachricht:",
- "desktop.dialog.cronExpression": "Cron-Ausdruck:",
- "desktop.dialog.cronHint": "Syntax: Min Std Tag Mon Wochentag (z.B. '*/15 * * * *' alle 15 Minuten)",
- "desktop.dialog.runOnStart": "Beim Anwendungsstart einmalig sofort ausführen",
- "desktop.dialog.addServiceTitle": "Dienst (Service) hinzufügen",
- "desktop.dialog.serviceName": "Name des Dienstes:",
- "desktop.dialog.serviceType": "Typ:",
- "desktop.dialog.servicePort": "Port:",
- "desktop.dialog.serviceDesc": "Beschreibung:",
- "desktop.dialog.restoreTitle": "Sicherung wiederherstellen",
- "desktop.dialog.backupInfo": "Sicherungsinformationen",
- "desktop.dialog.backupInstance": "Instanz: {0}",
- "desktop.dialog.backupCreated": "Erstellt: {0:g}",
- "desktop.dialog.backupFiles": "Dateien: {0}",
- "desktop.dialog.backupSecrets": "Zugangsdaten: {0}",
- "desktop.dialog.targetDir": "Zielordner:",
- "desktop.dialog.chooseFolder": "Ordner wählen",
- "desktop.dialog.restoreHint": "Hinweis: Es wird empfohlen, in einen neuen Ordner wiederherzustellen, um Konflikte mit laufenden Prozessen zu vermeiden.",
- "desktop.dialog.passphrase": "Passphrase:",
- "desktop.dialog.passphraseProtected": "Diese Sicherung enthält geschützte Zugangsdaten.",
- "desktop.dialog.overwriteExisting": "Vorhandene Dateien im Zielordner überschreiben",
- "desktop.dialog.licenseTitle": "ClawdDotNet – Lizenz",
- "desktop.dialog.licenseKey": "Lizenzschlüssel",
- "desktop.dialog.hardwareId": "Hardware-ID (für den Support):",
- "desktop.dialog.verify": "Prüfen",
- "desktop.dialog.textEditorTitle": "Text bearbeiten",
-
- "desktop.instancePicker.titleSwitch": "Instanz wechseln",
- "desktop.instancePicker.titleSelect": "Instanz wählen",
- "desktop.instancePicker.hintSwitch": "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten. Wechseln startet ClawdDotNet mit der gewählten Instanz neu.",
- "desktop.instancePicker.hintSelect": "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten.",
- "desktop.instancePicker.confirmSwitch": "Wechseln",
- "desktop.instancePicker.confirmStart": "Starten",
- "desktop.instancePicker.newInstancePlaceholder": "Name der neuen Instanz",
- "desktop.instancePicker.create": "Anlegen",
- "desktop.instancePicker.active": "Aktiv",
- "desktop.instancePicker.dirLabel": "Verzeichnis: {0}",
- "desktop.instancePicker.agentsCount": "{0} Agenten · API-Key {1}",
- "desktop.instancePicker.apiKeyConfigured": "konfiguriert",
- "desktop.instancePicker.apiKeyMissing": "fehlt",
- "desktop.instancePicker.tooltipConfigured": "API-Key hinterlegt",
- "desktop.instancePicker.tooltipMissing": "Kein API-Key — Agenten bleiben deaktiviert",
- "desktop.instancePicker.nameRequired": "Bitte einen Instanznamen eingeben.",
- "desktop.instancePicker.createFailed": "Instanz konnte nicht angelegt werden: {0}",
-
- "desktop.startMode.title": "ClawdDotNet – Startmodus wählen",
- "desktop.startMode.question": "Wie möchtest du ClawdDotNet starten?",
- "desktop.startMode.managerBtn": "Instanzmanager starten (Empfohlen)",
- "desktop.startMode.standaloneBtn": "Als Standalone-Instanz starten",
- "desktop.startMode.explanation": "• Instanzmanager (Empfohlen):\n Startet den zentralen Hintergrund-Manager und öffnet die Verwaltung im Browser. Ermöglicht den stabilen Dauerbetrieb mehrerer Instanzen parallel ohne Absturzrisiko.\n\n• Standalone-Modus:\n Startet genau eine Instanz direkt als einzelnes Desktop-Fenster.",
-
- "desktop.standaloneSwitch.title": "ClawdDotNet – Instanzwechsel (Standalone)",
- "desktop.standaloneSwitch.warning": "Achtung: Standalone-Modus",
- "desktop.standaloneSwitch.text": "Diese Instanz läuft eigenständig ohne Instanzmanager.\n\nWenn du die Instanz hier wechselst, wird diese Instanz vollständig beendet und alle laufenden Agenten und Hintergrunddienste werden gestoppt.\n\nEmpfehlung: Starte stattdessen den Instanzmanager, um mehrere Instanzen gleichzeitig und unabhängig voneinander zu betreiben.",
- "desktop.standaloneSwitch.switchAnyway": "Instanz trotzdem wechseln",
-
- "desktop.toolSettings.noSettings": "Dieses Werkzeug benötigt keine weiteren Einstellungen.",
- "desktop.toolSettings.host": "Host",
- "desktop.toolSettings.port": "Port",
- "desktop.toolSettings.username": "Benutzername",
- "desktop.toolSettings.password": "Passwort",
- "desktop.toolSettings.rootPath": "Root-Pfad",
- "desktop.toolSettings.imapHost": "IMAP-Host",
- "desktop.toolSettings.imapPort": "IMAP-Port",
- "desktop.toolSettings.smtpHost": "SMTP-Host",
- "desktop.toolSettings.smtpPort": "SMTP-Port",
- "desktop.toolSettings.allowedRecipients": "Erlaubte Empfänger",
- "desktop.toolSettings.type": "Typ",
- "desktop.toolSettings.connectionString": "Connection-String",
- "desktop.toolSettings.accessLevel": "Zugriffsebene",
- "desktop.toolSettings.allowedTables": "Erlaubte Tabellen",
- "desktop.toolSettings.botToken": "Bot-Token",
- "desktop.toolSettings.defaultChatId": "Standard Chat-ID",
- "desktop.toolSettings.allowedChatIds": "Erlaubte Chat-IDs",
- "desktop.toolSettings.defaultProvider": "Standard-Provider",
- "desktop.toolSettings.cacheTtl": "Cache TTL (Sekunden)",
- "desktop.toolSettings.twelveDataKey": "Twelve Data Key",
- "desktop.toolSettings.alphaVantageKey": "Alpha Vantage Key",
- "desktop.toolSettings.allowedDomains": "Erlaubte Domains",
- "desktop.toolSettings.maxResponseKb": "Max. Antwortgröße (KB)",
- "desktop.toolSettings.userAgent": "User Agent",
- "desktop.toolSettings.xApiKey": "X (Twitter) API-Key",
- "desktop.toolSettings.xAccounts": "X Accounts",
- "desktop.toolSettings.subreddits": "Subreddits",
- "desktop.toolSettings.redditPostLimit": "Reddit Post Limit",
- "desktop.toolSettings.openRouterKey": "OpenRouter Key",
- "desktop.toolSettings.sttModel": "STT-Modell",
- "desktop.toolSettings.youtubeChannels": "YouTube-Kanäle",
- "desktop.toolSettings.fileRwPersonalExt": "Erlaubte Dateiendungen (persönlich)",
- "desktop.toolSettings.fileRwSharedAccess": "Zugriffsebene (geteilt)",
- "desktop.toolSettings.fileRwSharedExt": "Erlaubte Dateiendungen (geteilt)",
- "desktop.toolSettings.fileRwProtectedPaths": "Geschützte Pfade",
-
- "nav.chat": "Chat",
- "nav.agents": "Agenten",
- "nav.tasks": "Aufgaben",
- "nav.usage": "Tokennutzung",
- "nav.agentChats": "Agenten-Chats",
- "nav.backup": "Sicherung",
- "nav.logs": "Protokoll",
- "nav.settings": "Einstellungen",
- "nav.info": "Info",
-
- "common.all": "Alle",
- "common.save": "Speichern",
- "common.cancel": "Abbrechen",
- "common.delete": "Löschen",
- "common.edit": "Bearbeiten",
- "common.refresh": "Aktualisieren",
- "common.close": "Schließen",
- "common.discard": "Verwerfen",
- "common.ready": "Bereit",
- "common.loading": "Lade…",
- "common.create": "Anlegen",
- "common.add": "Hinzufügen",
- "common.apply": "Übernehmen",
- "common.ok": "OK",
- "common.exit": "Beenden"
-}
diff --git a/src/ClawdDotNet.Desktop/i18n/en.json b/src/ClawdDotNet.Desktop/i18n/en.json
deleted file mode 100644
index 6590e4f..0000000
--- a/src/ClawdDotNet.Desktop/i18n/en.json
+++ /dev/null
@@ -1,341 +0,0 @@
-{
- "desktop.mainWindow.title": "ClawdDotNet",
- "desktop.mainWindow.sidebarTooltip": "Toggle sidebar",
- "desktop.mainWindow.switchInstance": "Switch instance",
- "desktop.mainWindow.instance": "INSTANCE",
- "desktop.mainWindow.themeLight": "Light Mode",
- "desktop.mainWindow.themeDark": "Dark Mode",
- "desktop.mainWindow.themeToggle": "Toggle theme",
- "desktop.mainWindow.openInstanceFolder": "Open Instance Folder",
- "desktop.mainWindow.instanceManager": "Instance Manager",
- "desktop.mainWindow.exit": "Exit",
- "desktop.mainWindow.instanceLabel": "Instance: {0}",
- "desktop.mainWindow.apiStatus": "API {0}",
- "desktop.mainWindow.credits": "Credits: ${0:F2} of ${1:F2}",
- "desktop.mainWindow.creditsNone": "Credits: —",
- "desktop.mainWindow.dismiss": "Dismiss",
-
- "desktop.nav.work": "Work",
- "desktop.nav.analytics": "Analytics",
- "desktop.nav.system": "System",
-
- "desktop.status.unknown": "unknown",
- "desktop.status.design": "Design",
- "desktop.status.noApiKey": "no API key",
- "desktop.status.noUsageData": "no usage data",
- "desktop.status.ready": "Ready",
- "desktop.status.reachable": "OK",
- "desktop.status.unreachable": "unreachable",
-
- "desktop.chat.agent": "Agent:",
- "desktop.chat.clearHistory": "Clear History",
- "desktop.chat.abort": "Abort",
- "desktop.chat.inputPlaceholder": "Type message… (Enter to send, Shift+Enter for new line)",
- "desktop.chat.send": "Send",
- "desktop.chat.statusReady": "Ready",
- "desktop.chat.statusSending": "Sending…",
- "desktop.chat.statusDesign": "Design Mode",
- "desktop.chat.noApiKey": "OpenRouter API key is not configured. Please enter the key in Settings.",
- "desktop.chat.noAgent": "Please select an agent.",
- "desktop.chat.emptyMessage": "Please enter a message.",
-
- "desktop.agents.addAgent": "Agent",
- "desktop.agents.removeAgent": "Agent",
- "desktop.agents.runNow": "Run Now",
- "desktop.agents.identity": "Identity",
- "desktop.agents.soul": "Soul",
- "desktop.agents.folder": "Folder",
- "desktop.agents.save": "Save",
- "desktop.agents.thDisplayName": "DISPLAY NAME",
- "desktop.agents.thModel": "MODEL",
- "desktop.agents.thSummary": "SUMMARY",
- "desktop.agents.kickerGeneralModel": "GENERAL & MODEL",
- "desktop.agents.agentId": "Agent ID",
- "desktop.agents.displayName": "Display Name",
- "desktop.agents.model": "Model",
- "desktop.agents.promptCaching": "Prompt Caching",
- "desktop.agents.roleDescription": "Role / Description",
- "desktop.agents.systemPrompt": "System Prompt (additional instructions)",
- "desktop.agents.kickerLimits": "LIMITS",
- "desktop.agents.limitsTitle": "Guardrails (LoopGuard & Budget)",
- "desktop.agents.maxSteps": "Max Steps per Run",
- "desktop.agents.timeoutSeconds": "Timeout (seconds)",
- "desktop.agents.maxContextTokens": "Max Context Tokens",
- "desktop.agents.maxToolResultChars": "Max Tool Result (chars)",
- "desktop.agents.dailyCost": "Daily Budget ($ USD)",
- "desktop.agents.dailyTokens": "Daily Budget (Tokens)",
- "desktop.agents.whenExhausted": "When the budget is exhausted (recurring tasks)",
- "desktop.agents.whenExhaustedHint": "expire: the missed run lapses and is recorded on the task. catchUpOnce: all missed runs are caught up by one run the next day, after the day's regular work. One-time tasks are always carried over to the next day.",
- "desktop.agents.allowedToolsFor": "ALLOWED TOOLS FOR {0}",
- "desktop.agents.thTool": "TOOL",
- "desktop.agents.thDescription": "DESCRIPTION",
- "desktop.agents.toolSettingsKicker": "TOOL SETTINGS",
- "desktop.agents.apply": "Apply",
- "desktop.agents.saved": "Agents saved successfully.",
-
- "desktop.tasks.tabJobs": "Scheduled Jobs",
- "desktop.tasks.tabServices": "Services",
- "desktop.tasks.tabHistory": "History",
- "desktop.tasks.addJob": "Add Job",
- "desktop.tasks.editJob": "Edit",
- "desktop.tasks.removeJob": "Remove",
- "desktop.tasks.toggleJob": "Enable/Disable",
- "desktop.tasks.runJobNow": "Run Now",
- "desktop.tasks.refresh": "Refresh",
- "desktop.tasks.thType": "TYPE",
- "desktop.tasks.thAgent": "AGENT",
- "desktop.tasks.thTask": "TOOL / MESSAGE",
- "desktop.tasks.thCron": "CRON",
- "desktop.tasks.thNextRun": "NEXT RUN",
- "desktop.tasks.thLastRun": "LAST RUN",
- "desktop.tasks.thStatus": "STATUS",
-
- "desktop.services.addService": "Add Service",
- "desktop.services.removeService": "Remove",
- "desktop.services.toggleService": "Enable/Disable",
- "desktop.services.thName": "SERVICE NAME",
- "desktop.services.thType": "TYPE",
- "desktop.services.thPort": "PORT",
- "desktop.services.thStatus": "STATUS",
- "desktop.services.thDescription": "DESCRIPTION",
-
- "desktop.history.refresh": "Refresh History",
- "desktop.history.thTime": "TIMESTAMP",
- "desktop.history.thJob": "JOB",
- "desktop.history.thAgent": "AGENT",
- "desktop.history.thDescription": "DESCRIPTION",
- "desktop.history.thStatus": "STATUS",
- "desktop.history.thDetails": "DETAILS",
-
- "desktop.usage.kickerTokens7d": "TOKENS (7 DAYS)",
- "desktop.usage.kickerCost7d": "COST (7 DAYS)",
- "desktop.usage.kickerRuns7d": "RUNS (7 DAYS)",
- "desktop.usage.refresh": "Refresh",
- "desktop.usage.tokensPerDay": "Tokens per Day",
- "desktop.usage.breakdownTitle": "Breakdown by Agent & Model",
- "desktop.usage.thAgent": "AGENT",
- "desktop.usage.thModel": "MODEL",
- "desktop.usage.thTokens": "TOKENS",
- "desktop.usage.thCost": "COST",
- "desktop.usage.thRuns": "RUNS",
- "desktop.usage.empty": "No token usage recorded yet.",
-
- "desktop.agentChats.agentA": "Agent A:",
- "desktop.agentChats.agentB": "Agent B:",
- "desktop.agentChats.empty": "No messages between these agents yet.",
- "desktop.agentChats.needTwo": "At least two agents are required in this instance to view a conversation history.",
-
- "desktop.backup.kickerManual": "MANUAL",
- "desktop.backup.titleCreate": "Create Backup",
- "desktop.backup.targetFolder": "Target Folder",
- "desktop.backup.targetFolderPlaceholder": "Target folder for backups…",
- "desktop.backup.browse": "Browse…",
- "desktop.backup.credentials": "Credentials",
- "desktop.backup.credentialsExclude": "do not include",
- "desktop.backup.credentialsProtect": "protect with passphrase",
- "desktop.backup.passphrase": "Enter passphrase",
- "desktop.backup.passphraseRepeat": "Repeat passphrase",
- "desktop.backup.scope": "Scope",
- "desktop.backup.scopeChat": "Chat history",
- "desktop.backup.scopeLogs": "Logs",
- "desktop.backup.createNow": "Create Backup Now",
- "desktop.backup.kickerAuto": "AUTOMATIC",
- "desktop.backup.titleAuto": "Automatic Backup",
- "desktop.backup.autoDaily": "daily backup at",
- "desktop.backup.time": "Time",
- "desktop.backup.keep": "Keep Count",
- "desktop.backup.autoHint": "Automatic backup excludes credentials — a saved passphrase would be ineffective.",
- "desktop.backup.kickerExisting": "EXISTING",
- "desktop.backup.titleExisting": "Existing Backups",
- "desktop.backup.refresh": "Refresh",
- "desktop.backup.openFolder": "Open Folder",
- "desktop.backup.delete": "Delete",
- "desktop.backup.restore": "Restore…",
- "desktop.backup.thFilename": "FILENAME",
- "desktop.backup.thCreatedAt": "CREATED AT",
- "desktop.backup.thInstance": "INSTANCE",
- "desktop.backup.thSize": "SIZE",
- "desktop.backup.thSecrets": "CREDENTIALS",
-
- "desktop.logs.module": "Module:",
- "desktop.logs.level": "Level:",
- "desktop.logs.refresh": "Refresh",
- "desktop.logs.clear": "Clear",
- "desktop.logs.openFolder": "Open Folder",
- "desktop.logs.follow": "Follow Tail",
-
- "desktop.settings.discard": "Discard",
- "desktop.settings.save": "Save",
- "desktop.settings.instance": "INSTANCE",
- "desktop.settings.instanceName": "Instance Name",
- "desktop.settings.apiKey": "OpenRouter API Key",
- "desktop.settings.autostart": "Autostart by Manager",
- "desktop.settings.autostartHint": "The instance manager starts this instance on launch and restarts it if it crashes.",
- "desktop.settings.logDir": "Log Directory",
- "desktop.settings.logDirHint": "Applies only to this instance. Empty means: Logs subfolder in instance directory.",
- "desktop.settings.dailyCost": "Daily Budget ($ USD)",
- "desktop.settings.dailyTokens": "Daily Budget (Tokens)",
- "desktop.settings.general": "GENERAL",
- "desktop.settings.instancesDir": "Instances Directory",
- "desktop.settings.minLogLevel": "Minimum Log Level",
- "desktop.settings.openRouterUrl": "OpenRouter Base URL",
- "desktop.settings.language": "Language",
- "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LICENSE",
- "desktop.settings.dcUrl": "Server URL",
- "desktop.settings.dcToken": "Master Token",
- "desktop.settings.dcEnv": "Environment",
- "desktop.settings.updateChannel": "Update Channel",
- "desktop.settings.licenseKey": "License Key",
- "desktop.settings.errorReporting": "Automatically report errors to Deploymentcenter",
- "desktop.settings.updateCheck": "Check for updates automatically on startup",
- "desktop.settings.saved": "Settings and instance configuration saved successfully.",
- "desktop.settings.savedLogRestart": "Saved. The log directory will take effect upon next restart.",
-
- "desktop.info.kickerApp": "APPLICATION",
- "desktop.info.build": "Build",
- "desktop.info.version": "Version",
- "desktop.info.buildSummary": "Build Summary",
- "desktop.info.kickerInstance": "INSTANCE",
- "desktop.info.currentInstance": "Current Instance",
- "desktop.info.openFolder": "Open Instance Folder",
- "desktop.info.name": "Name",
- "desktop.info.id": "ID",
- "desktop.info.path": "Path",
- "desktop.info.logDir": "Log Directory",
- "desktop.info.openRouter": "OpenRouter",
- "desktop.info.kickerModules": "MODULES",
- "desktop.info.assemblies": "Assemblies",
- "desktop.info.thModule": "MODULE",
- "desktop.info.thBuild": "BUILD",
- "desktop.info.thBuildDate": "BUILD DATE",
- "desktop.info.thChanges": "CHANGES",
-
- "desktop.dialog.addAgent": "Create New Agent",
- "desktop.dialog.agentId": "Agent ID (unique key):",
- "desktop.dialog.agentDisplayName": "Display Name:",
- "desktop.dialog.defaultModel": "Default Model:",
- "desktop.dialog.roleDescription": "Description / Role:",
- "desktop.dialog.addJobTitle": "Add / Edit Job",
- "desktop.dialog.jobType": "Job Type:",
- "desktop.dialog.agentWakeup": "Agent Wakeup (Periodic Execution)",
- "desktop.dialog.toolJob": "Tool Job (Tool-specific Job)",
- "desktop.dialog.jobTypeId": "Job Type ID / Name:",
- "desktop.dialog.taskMessage": "Task Description / Message:",
- "desktop.dialog.cronExpression": "Cron Expression:",
- "desktop.dialog.cronHint": "Syntax: Min Hour Day Month Weekday (e.g. '*/15 * * * *' every 15 minutes)",
- "desktop.dialog.runOnStart": "Run once immediately on application startup",
- "desktop.dialog.addServiceTitle": "Add Service",
- "desktop.dialog.serviceName": "Service Name:",
- "desktop.dialog.serviceType": "Type:",
- "desktop.dialog.servicePort": "Port:",
- "desktop.dialog.serviceDesc": "Description:",
- "desktop.dialog.restoreTitle": "Restore Backup",
- "desktop.dialog.backupInfo": "Backup Information",
- "desktop.dialog.backupInstance": "Instance: {0}",
- "desktop.dialog.backupCreated": "Created: {0:g}",
- "desktop.dialog.backupFiles": "Files: {0}",
- "desktop.dialog.backupSecrets": "Credentials: {0}",
- "desktop.dialog.targetDir": "Target Directory:",
- "desktop.dialog.chooseFolder": "Choose Folder",
- "desktop.dialog.restoreHint": "Note: It is recommended to restore into a new folder to avoid conflicts with running processes.",
- "desktop.dialog.passphrase": "Passphrase:",
- "desktop.dialog.passphraseProtected": "This backup contains protected credentials.",
- "desktop.dialog.overwriteExisting": "Overwrite existing files in target folder",
- "desktop.dialog.licenseTitle": "ClawdDotNet – License",
- "desktop.dialog.licenseKey": "License Key",
- "desktop.dialog.hardwareId": "Hardware ID (for support):",
- "desktop.dialog.verify": "Verify",
- "desktop.dialog.textEditorTitle": "Edit Text",
-
- "desktop.instancePicker.titleSwitch": "Switch Instance",
- "desktop.instancePicker.titleSelect": "Choose Instance",
- "desktop.instancePicker.hintSwitch": "Each instance runs independently with its own agents and data. Switching restarts ClawdDotNet with the selected instance.",
- "desktop.instancePicker.hintSelect": "Each instance runs independently with its own agents and data.",
- "desktop.instancePicker.confirmSwitch": "Switch",
- "desktop.instancePicker.confirmStart": "Start",
- "desktop.instancePicker.newInstancePlaceholder": "Name of new instance",
- "desktop.instancePicker.create": "Create",
- "desktop.instancePicker.active": "Active",
- "desktop.instancePicker.dirLabel": "Directory: {0}",
- "desktop.instancePicker.agentsCount": "{0} Agents · API Key {1}",
- "desktop.instancePicker.apiKeyConfigured": "configured",
- "desktop.instancePicker.apiKeyMissing": "missing",
- "desktop.instancePicker.tooltipConfigured": "API key configured",
- "desktop.instancePicker.tooltipMissing": "No API key — agents remain inactive",
- "desktop.instancePicker.nameRequired": "Please enter an instance name.",
- "desktop.instancePicker.createFailed": "Instance could not be created: {0}",
-
- "desktop.startMode.title": "ClawdDotNet – Select Start Mode",
- "desktop.startMode.question": "How would you like to start ClawdDotNet?",
- "desktop.startMode.managerBtn": "Launch Instance Manager (Recommended)",
- "desktop.startMode.standaloneBtn": "Start as Standalone Instance",
- "desktop.startMode.explanation": "• Instance Manager (Recommended):\n Starts the central background manager and opens management in your browser. Enables stable parallel operation of multiple instances without crash risks.\n\n• Standalone Mode:\n Starts exactly one instance directly as a single desktop window.",
-
- "desktop.standaloneSwitch.title": "ClawdDotNet – Instance Switch (Standalone)",
- "desktop.standaloneSwitch.warning": "Attention: Standalone Mode",
- "desktop.standaloneSwitch.text": "This instance runs standalone without the Instance Manager.\n\nIf you switch instances here, this instance will be fully stopped and all running agents and background services will end.\n\nRecommendation: Launch the Instance Manager instead to run multiple instances concurrently and independently.",
- "desktop.standaloneSwitch.switchAnyway": "Switch Instance Anyway",
-
- "desktop.toolSettings.noSettings": "This tool does not require additional settings.",
- "desktop.toolSettings.host": "Host",
- "desktop.toolSettings.port": "Port",
- "desktop.toolSettings.username": "Username",
- "desktop.toolSettings.password": "Password",
- "desktop.toolSettings.rootPath": "Root Path",
- "desktop.toolSettings.imapHost": "IMAP Host",
- "desktop.toolSettings.imapPort": "IMAP Port",
- "desktop.toolSettings.smtpHost": "SMTP Host",
- "desktop.toolSettings.smtpPort": "SMTP Port",
- "desktop.toolSettings.allowedRecipients": "Allowed Recipients",
- "desktop.toolSettings.type": "Type",
- "desktop.toolSettings.connectionString": "Connection String",
- "desktop.toolSettings.accessLevel": "Access Level",
- "desktop.toolSettings.allowedTables": "Allowed Tables",
- "desktop.toolSettings.botToken": "Bot Token",
- "desktop.toolSettings.defaultChatId": "Default Chat ID",
- "desktop.toolSettings.allowedChatIds": "Allowed Chat IDs",
- "desktop.toolSettings.defaultProvider": "Default Provider",
- "desktop.toolSettings.cacheTtl": "Cache TTL (seconds)",
- "desktop.toolSettings.twelveDataKey": "Twelve Data Key",
- "desktop.toolSettings.alphaVantageKey": "Alpha Vantage Key",
- "desktop.toolSettings.allowedDomains": "Allowed Domains",
- "desktop.toolSettings.maxResponseKb": "Max Response Size (KB)",
- "desktop.toolSettings.userAgent": "User Agent",
- "desktop.toolSettings.xApiKey": "X (Twitter) API Key",
- "desktop.toolSettings.xAccounts": "X Accounts",
- "desktop.toolSettings.subreddits": "Subreddits",
- "desktop.toolSettings.redditPostLimit": "Reddit Post Limit",
- "desktop.toolSettings.openRouterKey": "OpenRouter Key",
- "desktop.toolSettings.sttModel": "STT Model",
- "desktop.toolSettings.youtubeChannels": "YouTube Channels",
- "desktop.toolSettings.fileRwPersonalExt": "Allowed File Extensions (personal)",
- "desktop.toolSettings.fileRwSharedAccess": "Access Level (shared)",
- "desktop.toolSettings.fileRwSharedExt": "Allowed File Extensions (shared)",
- "desktop.toolSettings.fileRwProtectedPaths": "Protected Paths",
-
- "nav.chat": "Chat",
- "nav.agents": "Agents",
- "nav.tasks": "Tasks",
- "nav.usage": "Token Usage",
- "nav.agentChats": "Agent Chats",
- "nav.backup": "Backup",
- "nav.logs": "Logs",
- "nav.settings": "Settings",
- "nav.info": "Info",
-
- "common.all": "All",
- "common.save": "Save",
- "common.cancel": "Cancel",
- "common.delete": "Delete",
- "common.edit": "Edit",
- "common.refresh": "Refresh",
- "common.close": "Close",
- "common.discard": "Discard",
- "common.ready": "Ready",
- "common.loading": "Loading…",
- "common.create": "Create",
- "common.add": "Add",
- "common.apply": "Apply",
- "common.ok": "OK",
- "common.exit": "Exit"
-}
diff --git a/src/ClawdDotNet.Manager.Web/Endpoints.cs b/src/ClawdDotNet.Manager.Web/Endpoints.cs
index 0bbb055..7cbdfa3 100644
--- a/src/ClawdDotNet.Manager.Web/Endpoints.cs
+++ b/src/ClawdDotNet.Manager.Web/Endpoints.cs
@@ -164,25 +164,6 @@ internal static class ManagerEndpoints
};
});
- // Der Wechsel, wie E-M2 ihn meint: Alle laufen, eine kommt nach vorn. Frueher
- // hiess "Instanz wechseln", dass sich der laufende Prozess herunterfaehrt und mit
- // einer anderen Instanz neu startet - wer wechselte, beendete zwangslaeufig die
- // Instanz, aus der heraus er es tat.
- verwaltung.MapPost("/instanzen/{id}/fenster", async (
- string id, ManagerCore manager, InstanceRegistry registry, CancellationToken ct) =>
- {
- if (Find(registry, id) is not { } instanz)
- return Results.NotFound(new OutcomeDto(false, $"Es gibt keine Instanz '{id}'."));
-
- return await manager.FocusInstanceAsync(instanz.Directory, ct)
- ? Results.Ok(new OutcomeDto(true, $"Fenster von '{instanz.Name}' nach vorn geholt."))
- : Results.Json(
- new OutcomeDto(false,
- $"Instanz '{instanz.Name}' hat kein Fenster nach vorn zu holen — "
- + "sie läuft nicht oder läuft kopflos."),
- statusCode: StatusCodes.Status409Conflict);
- });
-
verwaltung.MapPost("/instanzen/{id}/stop", async (
string id, ManagerCore manager, InstanceRegistry registry, CancellationToken ct) =>
{
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
index 050b919..eeaf256 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json
@@ -781,7 +781,6 @@
"instances.selectActive": "Als aktiv wählen",
"instances.start": "Starten",
"instances.stop": "Beenden",
- "instances.bringToFront": "Fenster nach vorn",
"instances.createTitle": "Neue Instanz anlegen",
"instances.instanceOfflineError": "Die Instanz ist nicht erreichbar.",
"instances.noInstanceActiveError": "Keine Instanz aktiv. Unter 'Instanzen' starten oder auswählen.",
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
index 3404d67..8be69d6 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json
@@ -781,7 +781,6 @@
"instances.selectActive": "Select as active",
"instances.start": "Start",
"instances.stop": "Stop",
- "instances.bringToFront": "Bring window to front",
"instances.createTitle": "Create New Instance",
"instances.instanceOfflineError": "The instance is not reachable.",
"instances.noInstanceActiveError": "No instance is active. Start or select one under 'Instances'.",
diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/views/instances.js b/src/ClawdDotNet.Manager.Web/wwwroot/views/instances.js
index 4fbf8ac..b582b16 100644
--- a/src/ClawdDotNet.Manager.Web/wwwroot/views/instances.js
+++ b/src/ClawdDotNet.Manager.Web/wwwroot/views/instances.js
@@ -176,19 +176,7 @@ async function ladeInstanzen(istHintergrund = false) {
erstelleElement("span", { text: " " + t("instances.stop") })
);
- const fensterBtn = erstelleElement(
- "button",
- {
- klasse: "schaltflaeche flach klein",
- disabled: !istLaufend,
- title: t("instances.bringToFront"),
- onclick: () => holeFensterNachVorn(instanz)
- },
- erstelleIcon(PFADE.fenster, 14),
- erstelleElement("span", { text: " " + t("instances.bringToFront") })
- );
-
- fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn, fensterBtn);
+ fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn);
const karte = erstelleKarte(instanz.name, [dl, fussAktionen], {
aktionen: karteKopfAktionen,
@@ -336,17 +324,6 @@ function oeffneSicherheitsStoppDialog(instanz) {
modal.oeffnen();
}
-async function holeFensterNachVorn(instanz) {
- try {
- const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/fenster`, {
- method: "POST"
- });
- zeigeMeldung(antwort?.meldung || `Brought '${instanz.name}' to front.`, "info");
- } catch (err) {
- zeigeMeldung(`Window focus failed: ${err.message}`, "warnung");
- }
-}
-
function oeffneNeueInstanzDialog() {
const formular = erstelleElement("form", {
klasse: "formular-raster",
diff --git a/src/ClawdDotNet.Manager/InstanceChannel.cs b/src/ClawdDotNet.Manager/InstanceChannel.cs
index ff39826..75ff499 100644
--- a/src/ClawdDotNet.Manager/InstanceChannel.cs
+++ b/src/ClawdDotNet.Manager/InstanceChannel.cs
@@ -58,14 +58,6 @@ public interface IInstanceChannel
/// Socket.
///
Task RequestStopAsync(string instanceDirectory, CancellationToken ct = default);
-
- ///
- /// Bittet die Instanz, ihr Fenster nach vorn zu holen (E-M2).
- ///
- /// false heißt: Die Bitte kam nicht an, oder diese Instanz hat kein
- /// Fenster — sie läuft kopflos. Beides ist kein Fehler, sondern eine Auskunft.
- ///
- Task RequestFocusAsync(string instanceDirectory, CancellationToken ct = default);
}
///
@@ -135,10 +127,6 @@ public sealed class SocketInstanceChannel(TimeSpan? timeout = null) : IInstanceC
}
}
- public Task RequestFocusAsync(
- string instanceDirectory, CancellationToken ct = default)
- => PostAsync(instanceDirectory, "/api/instanz/fenster", ct);
-
public Task RequestStopAsync(
string instanceDirectory, CancellationToken ct = default)
=> PostAsync(instanceDirectory, "/api/instanz/beenden", ct);
diff --git a/src/ClawdDotNet.Manager/InstanceLauncher.cs b/src/ClawdDotNet.Manager/InstanceLauncher.cs
index 8790369..49f8599 100644
--- a/src/ClawdDotNet.Manager/InstanceLauncher.cs
+++ b/src/ClawdDotNet.Manager/InstanceLauncher.cs
@@ -56,11 +56,14 @@ public interface IInstanceLauncher
/// Auf einer Maschine ohne Bildschirmsitzung gibt es keines, und dann ist kopflos die
/// einzige Betriebsart, die trägt.
///
+/// #764: Seit die Fenster entfernt sind, ist kopflos die einzige Betriebsart —
+/// --headless geht immer mit.
+///
/// --managed geht immer mit. Daran erkennt die Instanz, dass sie
/// geführt wird, und bietet den Instanzwechsel nicht mehr selbst an: Der wäre ein
/// Selbst-Neustart und nähme genau die Instanz mit, aus der heraus jemand wechselt.
///
-public sealed class ProcessInstanceLauncher(string executablePath, bool headless = true)
+public sealed class ProcessInstanceLauncher(string executablePath)
: IInstanceLauncher
{
/// Wo die Programmdatei liegt — in der Regel neben der des Managers.
@@ -87,7 +90,7 @@ public sealed class ProcessInstanceLauncher(string executablePath, bool headless
CreateNoWindow = true
};
- if (headless) start.ArgumentList.Add("--headless");
+ start.ArgumentList.Add("--headless");
start.ArgumentList.Add("--managed");
start.ArgumentList.Add("--instance");
diff --git a/src/ClawdDotNet.Manager/InstanceRegistry.cs b/src/ClawdDotNet.Manager/InstanceRegistry.cs
index 8cf33cc..a0b4ce3 100644
--- a/src/ClawdDotNet.Manager/InstanceRegistry.cs
+++ b/src/ClawdDotNet.Manager/InstanceRegistry.cs
@@ -215,28 +215,6 @@ public sealed class InstanceRegistry : IAsyncDisposable
public Task StopAsync(string instanceDirectory, CancellationToken ct = default)
=> Find(instanceDirectory).StopAsync(ct);
- ///
- /// Holt das Fenster einer Instanz nach vorn — der Wechsel, wie E-M2 ihn meint.
- ///
- /// Das ist der Ersatz für den Selbst-Neustart. Vorher hiess „Instanz
- /// wechseln": Der laufende Prozess fährt sich herunter und startet sich mit einer
- /// anderen Instanz neu — wer wechselte, beendete zwangsläufig die Instanz, aus der
- /// heraus er es tat. Jetzt laufen alle, und es kommt nur eine nach vorn.
- ///
- /// false heisst: Sie läuft nicht, sie hört nicht, oder sie hat kein
- /// Fenster.
- ///
- public async Task FocusAsync(string instanceDirectory, CancellationToken ct = default)
- {
- var aufsicht = Find(instanceDirectory);
-
- if (aufsicht.Condition != InstanceCondition.Running)
- return false;
-
- return await _channel.RequestFocusAsync(aufsicht.InstanceDirectory, ct)
- .ConfigureAwait(false);
- }
-
///
/// Hält alle laufenden Instanzen an. Nicht beim Beenden des Managers, sondern
/// nur, wenn es einen Grund gibt, der sie alle betrifft — ein Lizenzwiderruf.
diff --git a/src/ClawdDotNet.Manager/ManagerCore.cs b/src/ClawdDotNet.Manager/ManagerCore.cs
index 2ebef3d..1bc19d3 100644
--- a/src/ClawdDotNet.Manager/ManagerCore.cs
+++ b/src/ClawdDotNet.Manager/ManagerCore.cs
@@ -298,9 +298,6 @@ public sealed class ManagerCore : IAsyncDisposable
public Task StopInstanceAsync(string instanceDirectory, CancellationToken ct = default)
=> Instances.StopAsync(instanceDirectory, ct);
- ///
- public Task FocusInstanceAsync(string instanceDirectory, CancellationToken ct = default)
- => Instances.FocusAsync(instanceDirectory, ct);
// ─── Das Urteil ───
diff --git a/src/ClawdDotNet.Manager/ManagerSettings.cs b/src/ClawdDotNet.Manager/ManagerSettings.cs
index ecf7759..c7df709 100644
--- a/src/ClawdDotNet.Manager/ManagerSettings.cs
+++ b/src/ClawdDotNet.Manager/ManagerSettings.cs
@@ -47,6 +47,11 @@ public sealed class ManagerSettings
/// Bildschirmsitzung — einem Server, einem Bauknecht — gehört hier false hin;
/// dann laufen die Instanzen kopflos und sind nur über die Weboberfläche zu
/// bedienen.
+ ///
+ /// #764: wird nicht mehr ausgewertet. Die Avalonia-Fenster sind entfernt;
+ /// jede Instanz läuft kopflos. Der Schlüssel bleibt lesbar, damit eine bestehende
+ /// manager.json ihn nicht als unbekannt mitschleppt oder beim Laden scheitert —
+ /// gelöscht wird er beim nächsten Speichern nicht, er tut nur nichts mehr.
///
[JsonPropertyName("instanceWindows")]
public bool InstanceWindows { get; set; } = true;
diff --git a/src/ClawdDotNet.ManagerApp/Program.cs b/src/ClawdDotNet.ManagerApp/Program.cs
index 9eed6d9..a4e6fdf 100644
--- a/src/ClawdDotNet.ManagerApp/Program.cs
+++ b/src/ClawdDotNet.ManagerApp/Program.cs
@@ -73,8 +73,7 @@ internal static class Program
: settings.Settings.InstanceExecutable);
logger.LogInformation(
- "Instanzen laufen {Art}. Neustart nach Absturz: hoechstens {Grenze} in {Fenster}.",
- settings.Settings.InstanceWindows ? "mit eigenem Fenster" : "ohne Fenster",
+ "Instanzen laufen ohne Fenster. Neustart nach Absturz: hoechstens {Grenze} in {Fenster}.",
settings.Settings.ToRestartPolicy().MaxRestarts,
settings.Settings.ToRestartPolicy().Window);
@@ -166,7 +165,7 @@ internal static class Program
Path.GetFullPath(s.InstancesDirectory));
var launcher = new ProcessInstanceLauncher(
- ResolveInstanceExecutable(s, logger), headless: !s.InstanceWindows);
+ ResolveInstanceExecutable(s, logger));
var channel = new SocketInstanceChannel();
var registry = new InstanceRegistry(
diff --git a/src/ClawdDotNet.Web/Endpoints/ControlEndpoints.cs b/src/ClawdDotNet.Web/Endpoints/ControlEndpoints.cs
index ae01b94..edea8a6 100644
--- a/src/ClawdDotNet.Web/Endpoints/ControlEndpoints.cs
+++ b/src/ClawdDotNet.Web/Endpoints/ControlEndpoints.cs
@@ -40,6 +40,9 @@ public sealed record PulsDto(
/// Der Endpunkt löst aus, der Wirtsprozess reagiert. Und wer reagiert, ist verschieden —
/// kopflos die Warteschleife in HeadlessRun, mit Fenster die
/// Avalonia-Anwendung.
+///
+/// #764: Seit die Avalonia-Fenster entfernt sind, ist HeadlessRun der einzige
+/// Wirt. Mit den Fenstern ging auch „Fenster nach vorn holen" (E-M2): Es gibt keines mehr.
///
public sealed class InstanceControl
{
@@ -48,14 +51,6 @@ public sealed class InstanceControl
/// Der Manager bittet, diesen Prozess zu beenden.
public event Action? ShutdownRequested;
- ///
- /// Der Manager bittet, das Fenster dieser Instanz nach vorn zu holen (E-M2).
- ///
- /// Kopflos gibt es nichts nach vorn zu holen; dann läuft dieses Ereignis ins
- /// Leere, und das ist die richtige Antwort.
- ///
- public event Action? FocusRequested;
-
///
/// Meldet den Wunsch zu enden an. Ein zweiter Aufruf tut nichts — sonst stiesse er
/// mitten im Herunterfahren ein zweites Herunterfahren an.
@@ -69,11 +64,6 @@ public sealed class InstanceControl
return true;
}
- ///
- /// Holt das Fenster nach vorn. Beliebig oft — anders als beim Beenden ist die
- /// Wiederholung hier der Normalfall: Jeder Wechsel auf diese Instanz ist einer.
- ///
- public void RequestFocus() => FocusRequested?.Invoke();
}
///
@@ -83,8 +73,8 @@ public sealed class InstanceControl
/// Endpunkte nicht, auch mit gültigem Token nicht, und sie antworten dort mit 404 statt
/// mit 403: Was es nicht gibt, muss man auch nicht erklären. Der Grund ist der Schnitt aus
/// #528 — die Lebensdauer einer Instanz gehört dem Instanzmanager. Eine Browsersitzung
-/// soll den Prozess, der sie gerade bedient, nicht beenden und auch nicht sein Fenster
-/// aufreissen können; der Manager soll es, und er kommt über den Socket.
+/// soll den Prozess, der sie gerade bedient, nicht beenden können (bis #764 auch nicht sein
+/// Fenster aufreissen); der Manager soll es, und er kommt über den Socket.
///
internal static class ControlEndpoints
{
@@ -130,18 +120,5 @@ internal static class ControlEndpoints
// 202 und nicht 200: Beim Antworten laeuft der Prozess noch.
return Results.Accepted(value: new { angenommen = true });
});
-
- control.MapPost("/fenster", (HttpContext context, InstanceControl steuerung) =>
- {
- if (!LocalTransport.Applies(context))
- return Results.NotFound();
-
- // Hier ohne OnCompleted: Das Fenster nach vorn zu holen beendet nichts, die
- // Antwort geht ohnehin raus. Und der Manager soll gleich erfahren, ob dieser
- // Prozess ueberhaupt eines hat.
- steuerung.RequestFocus();
-
- return Results.Ok(new { angenommen = true });
- });
}
}
diff --git a/src/ClawdDotNet.Web/InstanceHttpHost.cs b/src/ClawdDotNet.Web/InstanceHttpHost.cs
index dd22ddc..c1f8e9b 100644
--- a/src/ClawdDotNet.Web/InstanceHttpHost.cs
+++ b/src/ClawdDotNet.Web/InstanceHttpHost.cs
@@ -56,8 +56,8 @@ public sealed class InstanceHttpHost : IAsyncDisposable
///
/// Was der Instanzmanager diesem Prozess über den Socket sagen kann: sich beenden
- /// (POST /api/instanz/beenden) oder sein Fenster nach vorn holen
- /// (POST /api/instanz/fenster).
+ /// (POST /api/instanz/beenden). Bis #764 auch: sein Fenster nach vorn holen —
+ /// das Fenster gibt es nicht mehr.
///
/// Der Wirtsprozess hängt sich hier an und tut es: kopflos beendet sich die
/// Warteschleife, mit Fenster reagiert die Avalonia-Anwendung. Diese Klasse tut es
diff --git a/tests/ClawdDotNet.Manager.Tests/Doubles.cs b/tests/ClawdDotNet.Manager.Tests/Doubles.cs
index 4880cf8..4ca4b68 100644
--- a/tests/ClawdDotNet.Manager.Tests/Doubles.cs
+++ b/tests/ClawdDotNet.Manager.Tests/Doubles.cs
@@ -186,16 +186,4 @@ internal sealed class FakeChannel : IInstanceChannel
OnStopRequest?.Invoke(instanceDirectory);
return Task.FromResult(StopRequestArrives);
}
-
- /// Welche Instanzen gebeten wurden, ihr Fenster nach vorn zu holen.
- public List FocusRequests { get; } = [];
-
- /// Ob die Instanz ein Fenster hat, das nach vorn kommen kann.
- public bool HasWindow { get; set; } = true;
-
- public Task RequestFocusAsync(string instanceDirectory, CancellationToken ct = default)
- {
- FocusRequests.Add(Path.GetFullPath(instanceDirectory));
- return Task.FromResult(HasWindow);
- }
}
diff --git a/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs b/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs
index 24c3356..ea47bd9 100644
--- a/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs
+++ b/tests/ClawdDotNet.Manager.Tests/ManagerCoreTests.cs
@@ -222,98 +222,3 @@ public sealed class ManagerCoreTests : IAsyncDisposable
(await manager.StartInstanceAsync(pfad)).ShouldBe(StartRefusal.DidNotStart);
}
}
-
-///
-/// Der Wechsel, wie E-M2 ihn meint (#528, Stufe 5).
-///
-/// Vorher hiess „Instanz wechseln": Der laufende Prozess fährt sich herunter
-/// und startet sich mit einer anderen Instanz neu. Wer wechselte, beendete zwangsläufig
-/// die Instanz, aus der heraus er es tat — und genau dabei hat sich am 2026-09-08 die
-/// Anwendung aufgehängt (#529). Jetzt laufen alle, und es kommt nur eine nach vorn.
-///
-public sealed class InstanceFocusTests : IAsyncDisposable
-{
- private readonly string _root = Path.Combine(
- Path.GetTempPath(), "clawd-tests", "focus-" + Guid.NewGuid().ToString("N")[..8]);
-
- private readonly FakeLauncher _launcher = new();
- private readonly FakeChannel _kanal = new();
- private readonly InstanceDirectoryManager _verzeichnisse;
-
- private static readonly SupervisionOptions Options = new(
- ProbeInterval: TimeSpan.FromMilliseconds(50),
- StartTimeout: TimeSpan.FromMilliseconds(600),
- SilentChecksBeforeAlarm: 3,
- StopGrace: TimeSpan.FromMilliseconds(300),
- SilentKillGrace: TimeSpan.FromMilliseconds(300),
- MaxDrainWait: TimeSpan.FromSeconds(2));
-
- public InstanceFocusTests()
- {
- Directory.CreateDirectory(_root);
- _verzeichnisse = new InstanceDirectoryManager(Path.Combine(_root, "Instances"));
- // Der Socket antwortet nur, wenn wirklich ein Prozess laeuft - genau wie im
- // Betrieb (siehe FakeChannel.Launcher).
- _kanal.Launcher = _launcher;
- }
-
- private InstanceRegistry Register() => new(
- _verzeichnisse, _launcher, _kanal, NullLoggerFactory.Instance, Options);
-
- public async ValueTask DisposeAsync()
- {
- await Task.CompletedTask;
- try { Directory.Delete(_root, recursive: true); }
- catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
- }
-
- [Fact]
- public async Task Eine_laufende_Instanz_wird_nach_vorn_geholt()
- {
- await using var register = Register();
- var pfad = register.Create("Alpha");
-
- _kanal.AnswerFor[Path.GetFullPath(pfad)] = new InstanceIdentity("i-1", "Alpha", "0.1.2");
- await register.StartAsync(pfad);
-
- (await register.FocusAsync(pfad)).ShouldBeTrue();
-
- _kanal.FocusRequests.ShouldBe([Path.GetFullPath(pfad)]);
-
- // Und vor allem: Es wurde nichts beendet und nichts neu gestartet.
- _launcher.Starts.ShouldBe(1);
- _kanal.StopRequests.ShouldBe(0);
- _launcher.Last!.HasExited.ShouldBeFalse();
- }
-
- [Fact]
- public async Task Eine_Instanz_die_nicht_laeuft_hat_kein_Fenster()
- {
- await using var register = Register();
- var pfad = register.Create("Alpha");
-
- (await register.FocusAsync(pfad)).ShouldBeFalse();
-
- // Nicht einmal gefragt: Wer nicht laeuft, antwortet auch nicht.
- _kanal.FocusRequests.ShouldBeEmpty();
- }
-
- ///
- /// Eine kopflose Instanz hat nichts nach vorn zu holen — und sagt das, statt Erfolg
- /// zu melden.
- ///
- [Fact]
- public async Task Eine_kopflose_Instanz_meldet_dass_sie_kein_Fenster_hat()
- {
- await using var register = Register();
- var pfad = register.Create("Alpha");
-
- _kanal.AnswerFor[Path.GetFullPath(pfad)] = new InstanceIdentity("i-1", "Alpha", "0.1.2");
- await register.StartAsync(pfad);
-
- _kanal.HasWindow = false;
-
- (await register.FocusAsync(pfad)).ShouldBeFalse();
- _kanal.FocusRequests.ShouldHaveSingleItem();
- }
-}
diff --git a/tests/ClawdDotNet.Web.Tests/InstanceSocketTests.cs b/tests/ClawdDotNet.Web.Tests/InstanceSocketTests.cs
index fd626f0..c6ad92f 100644
--- a/tests/ClawdDotNet.Web.Tests/InstanceSocketTests.cs
+++ b/tests/ClawdDotNet.Web.Tests/InstanceSocketTests.cs
@@ -247,27 +247,6 @@ public sealed class InstanceControlTests : IDisposable
return port;
}
- [Fact]
- public async Task Ueber_den_Socket_kommt_die_Bitte_um_das_Fenster_an()
- {
- await using var host = NeuerHost(FreierPort());
- await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance);
-
- web.ShouldNotBeNull();
-
- var gebeten = 0;
- web!.Control.FocusRequested += () => Interlocked.Increment(ref gebeten);
-
- using var client = InstanceEndpoint.CreateClient(host.InstancePath);
- (await client.PostAsync("/api/instanz/fenster", content: null)).EnsureSuccessStatusCode();
-
- gebeten.ShouldBe(1);
-
- // Und beliebig oft: Jeder Wechsel auf diese Instanz ist einer.
- (await client.PostAsync("/api/instanz/fenster", content: null)).EnsureSuccessStatusCode();
- gebeten.ShouldBe(2);
- }
-
[Fact]
public async Task Ueber_TCP_gibt_es_die_Steuerung_nicht()
{
@@ -280,15 +259,12 @@ public sealed class InstanceControlTests : IDisposable
web!.Url.ShouldNotBeNull();
var gebeten = false;
- web.Control.FocusRequested += () => gebeten = true;
+ web.Control.ShutdownRequested += () => gebeten = true;
using var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}") };
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Token}");
// Mit gueltigem Token - und trotzdem nicht vorhanden.
- (await client.PostAsync("/api/instanz/fenster", content: null)).StatusCode
- .ShouldBe(System.Net.HttpStatusCode.NotFound);
-
(await client.PostAsync("/api/instanz/beenden", content: null)).StatusCode
.ShouldBe(System.Net.HttpStatusCode.NotFound);