90 Commits

Author SHA1 Message Date
mika kuns
edfb702ecc fix(data): track EF migration Designer files (were gitignored)
All checks were successful
Release / release (push) Successful in 33s
The `*.designer.cs` ignore rule silently excluded EF Core migration
*.Designer.cs files, so only 1 of 11 was committed. Without the Designer
(which carries the [Migration] attribute), EF does not register a migration,
so a fresh clone / CI release build could not apply migrations — e.g.
GetAppSettings failed with "no such column: repo_import_folders", which the
Settings modal surfaced as "Worker offline".

Adds a .gitignore negation for **/Migrations/*.Designer.cs and commits the
10 missing Designer files (incl. the newly authored AddRepoImportFolders).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 13:15:25 +02:00
mika kuns
549b87bb74 docs: reflect Startup-shortcut worker autostart
All checks were successful
Release / release (push) Successful in 34s
Replace Windows-service/scheduled-task deployment docs with the Startup-folder
shortcut mechanism and the App's connection-failure prompt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:34:23 +02:00
mika kuns
400a078aec refactor(installer): rename StopWorkerStep.TaskName to LegacyTaskName
The schtasks delete is now only legacy-migration cleanup; current installs
autostart via a Startup-folder shortcut. Clarifies the constant and comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:21:56 +02:00
mika kuns
5baa1d7fbb docs: add worker lifecycle implementation plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:19:32 +02:00
mika kuns
1246bf7b88 feat(ui): wire worker connection modal and make status pill clickable 2026-06-01 12:18:28 +02:00
mika kuns
00dc7ebccc feat(ui): prompt once on worker connection failure with grace timer
Adds ShowWorkerConnectionModal hook, DecideShowConnectionPrompt one-shot gate, OpenWorkerConnectionHelp relay command, and a 12 s _connectTimer to IslandsShellViewModel; covered by two new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:17:01 +02:00
mika kuns
0139607008 feat(ui): add worker connection help modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:14:36 +02:00
mika kuns
4ecd855fb1 refactor(ui): stop auto-spawning the worker on app start 2026-06-01 12:12:49 +02:00
mika kuns
759d9057ff feat(installer): remove Startup worker shortcut on uninstall 2026-06-01 12:11:28 +02:00
mika kuns
2f1dcdc102 feat(installer): start worker via Process.Start, drop schtasks stop 2026-06-01 12:10:28 +02:00
mika kuns
133f2d2f1d feat(installer): register autostart via Startup shortcut, drop scheduled task
Replaces schtasks /Create with AutostartShortcut.Install; migrates away
legacy scheduled task and Windows service on upgrade. Removes ScheduledTaskXml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:09:07 +02:00
mika kuns
e2bb43ad6d feat(installer): add AutostartShortcut helper for Startup-folder lnk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:07:32 +02:00
mika kuns
867dc37228 refactor(installer): extract ShortcutFactory COM helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:05:54 +02:00
mika kuns
4963a726de docs: add worker lifecycle redesign spec
Startup-folder shortcut replaces the scheduled task; App only connects and
prompts on connection failure instead of auto-spawning a worker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:55:08 +02:00
mika kuns
926471da6b refactor(ui): migrate PlanningDiffView to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:22 +02:00
mika kuns
9be8e6b3e0 refactor(ui): drop double padding in Tasks island header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:17 +02:00
mika kuns
b9e5dfccde refactor(ui): drop double padding in Lists island header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:12 +02:00
mika kuns
c669370ecf refactor(ui): class schedule-flyout cancel in TaskRowView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:08 +02:00
mika kuns
4688e884bd refactor(ui): class merge-section buttons in DetailsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:57:03 +02:00
mika kuns
8b21b0e646 refactor(ui): class update-banner buttons in MainWindow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:56:58 +02:00
mika kuns
4a786eb732 refactor(ui): normalize buttons/footer/padding in ConflictResolutionView 2026-05-30 18:54:17 +02:00
mika kuns
cd64f287c3 refactor(ui): normalize buttons/footer/padding in DiffModal 2026-05-30 18:53:49 +02:00
mika kuns
3585ad5ee2 refactor(ui): normalize buttons/footer/padding in WorktreesOverviewModal 2026-05-30 18:53:29 +02:00
mika kuns
990935e67d refactor(ui): normalize buttons/footer/padding in RepoImportModal 2026-05-30 18:53:07 +02:00
mika kuns
1b5a9285e6 refactor(ui): normalize buttons/footer/padding in UnfinishedPlanningModal 2026-05-30 18:52:18 +02:00
mika kuns
e8f880e72f refactor(ui): normalize buttons/footer/padding in AboutModal 2026-05-30 18:51:35 +02:00
mika kuns
3228a08c7a refactor(ui): normalize buttons/footer/padding in MergeModal 2026-05-30 18:50:57 +02:00
mika kuns
ccec791fc1 refactor(ui): normalize buttons/footer/padding in ListSettingsModal 2026-05-30 18:50:32 +02:00
mika kuns
187fb641fe refactor(ui): normalize buttons/footer/padding in SettingsModal 2026-05-30 18:49:49 +02:00
mika kuns
0a719568ea refactor(ui): make primary/danger buttons self-contained, drop unused btn.primary 2026-05-30 18:47:17 +02:00
mika kuns
ccec591ba2 refactor(ui): inherit terminal font for SelectableTextBlock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:54:16 +02:00
mika kuns
a4cb03b1b5 refactor(ui): use sidebar-pane in PlanningDiffView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:53:56 +02:00
mika kuns
f53292e134 refactor(ui): use diff-lineno and sidebar-pane in DiffModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:53:34 +02:00
mika kuns
539ebecf3a refactor(ui): use danger-box in MergeModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:53:07 +02:00
mika kuns
dff5651db7 refactor(ui): use danger-box in SettingsModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:52:49 +02:00
mika kuns
9f49b0131f refactor(ui): use shared section style in ListSettingsModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:52:30 +02:00
mika kuns
fb3a6acf52 refactor(ui): reuse task-row style for worktree rows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:52:12 +02:00
mika kuns
4f84b15b6a refactor(ui): use section-divider in DetailsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:51:46 +02:00
mika kuns
27b0d51db0 refactor(ui): drop duplicate converters and normalize binding in ListsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:51:19 +02:00
mika kuns
2a381048fe refactor(ui): merge task-row styles and add shared section/danger-box/sidebar/accent styles 2026-05-30 17:49:24 +02:00
mika kuns
bddef5abef refactor(ui): unify text and close button in ThemedDatePicker 2026-05-30 17:40:07 +02:00
mika kuns
51d3ea2e1c refactor(ui): unify text and close button in ConflictResolutionView 2026-05-30 17:39:18 +02:00
mika kuns
335b422e23 refactor(ui): unify text and close button in PlanningDiffView 2026-05-30 17:38:44 +02:00
mika kuns
08f3babca4 refactor(ui): unify text and close button in DiffModalView 2026-05-30 17:38:09 +02:00
mika kuns
9082f2ed71 refactor(ui): unify text and close button in WorktreeModalView 2026-05-30 17:37:06 +02:00
mika kuns
0f64b1c6e0 refactor(ui): unify text and close button in WorktreesOverviewModalView 2026-05-30 17:36:23 +02:00
mika kuns
dd453874ba refactor(ui): unify text and close button in RepoImportModalView 2026-05-30 17:33:52 +02:00
mika kuns
00e1d2d6c9 refactor(ui): unify text and close button in UnfinishedPlanningModalView 2026-05-30 17:33:29 +02:00
mika kuns
9a9113542d refactor(ui): unify text and close button in AboutModalView 2026-05-30 17:33:06 +02:00
mika kuns
8e595a1e43 refactor(ui): unify text and close button in MergeModalView 2026-05-30 17:32:29 +02:00
mika kuns
97fc715856 refactor(ui): unify text and close button in ListSettingsModalView 2026-05-30 17:32:02 +02:00
mika kuns
ed8607d4c9 refactor(ui): unify text and close button in SettingsModalView 2026-05-30 17:31:31 +02:00
mika kuns
929e0ca1ee refactor(ui): apply text classes to SessionTerminalView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:29:04 +02:00
mika kuns
40a36308ae refactor(ui): apply text classes to AgentStripView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:28:35 +02:00
mika kuns
b9f5d829c8 refactor(ui): apply text classes to TaskRowView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:27:49 +02:00
mika kuns
e0dda3e71b refactor(ui): apply text classes to DetailsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:27:13 +02:00
mika kuns
d4c66dea63 refactor(ui): apply text classes to TasksIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:26:10 +02:00
mika kuns
a132127e9e refactor(ui): apply text classes to ListsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:25:48 +02:00
mika kuns
6e3125e78d refactor(ui): apply text classes to MainWindow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 17:24:58 +02:00
mika kuns
b00e4d994f feat(ui): unify type scale to 11/13/18/24 and add canonical text classes 2026-05-30 17:22:29 +02:00
mika kuns
16717ab9e9 fix(ui): restore resize and full-width rows in WorktreesOverview modal 2026-05-30 17:16:08 +02:00
mika kuns
7af892f410 refactor(ui): consolidate list-section-label into shared section-label 2026-05-30 17:07:47 +02:00
mika kuns
e86464e802 fix(ui): unclip Edit/Preview buttons; enlarge section labels and use mono field labels 2026-05-30 17:02:35 +02:00
mika kuns
df7337810e docs(ui): add visual-check checklist for normalization pass 2026-05-30 16:53:36 +02:00
mika kuns
8944074997 refactor(ui): fold selected-day White to TextBrush token 2026-05-30 16:52:56 +02:00
mika kuns
fbd5d9f7ca refactor(ui): tokenize WorktreeModalView font sizes 2026-05-30 16:52:16 +02:00
mika kuns
5fdd9f0b4c refactor(ui): tokenize and dynamic-ize PlanningDiffView
Convert StaticResource token attrs to DynamicResource, snap font sizes to tokens, replace Consolas,Menlo,monospace with MonoFont DynamicResource, and fold Orange warning color to BloodBrush.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:50:43 +02:00
mika kuns
bce4e0a1e6 refactor(ui): migrate ConflictResolutionView to ModalShell and use dynamic resources
Replace manual titlebar/drag handler with ModalShell, move action buttons to footer, convert StaticResource token attrs to DynamicResource, replace OrangeRed with BloodBrush, and use MonoFont DynamicResource.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:50:38 +02:00
mika kuns
229f865e7e refactor(ui): migrate DiffModal to ModalShell and use dynamic resources
Replace manual titlebar/drag handler with ModalShell, move Merge button to footer, convert StaticResource token attrs to DynamicResource, snap font sizes to tokens, use MonoFont DynamicResource, and fold tint color literals to RunningTintBrush/ErrorTintBrush.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:50:32 +02:00
mika kuns
a444033aa9 refactor(ui): migrate WorktreesOverviewModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:47:32 +02:00
mika kuns
2265829a29 refactor(ui): migrate RepoImportModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:45:51 +02:00
mika kuns
50e05b9140 refactor(ui): migrate UnfinishedPlanningModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:45:16 +02:00
mika kuns
538839c004 refactor(ui): migrate AboutModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:44:41 +02:00
mika kuns
8d07fc298c refactor(ui): migrate MergeModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:44:04 +02:00
mika kuns
e1bfbb0fa6 refactor(ui): migrate ListSettingsModal to ModalShell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:43:17 +02:00
mika kuns
b1006ac7b0 fix(ui): correct SettingsModal font snap (11px is Mono, not Body) 2026-05-30 16:41:05 +02:00
mika kuns
4f5db367a7 refactor(ui): migrate SettingsModal to ModalShell 2026-05-30 16:40:09 +02:00
mika kuns
c20fbe3613 feat(ui): add reusable ModalShell control 2026-05-30 16:38:02 +02:00
mika kuns
16b0d1177a refactor(ui): tokenize ThemedDatePicker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:36:23 +02:00
mika kuns
a1f05da97b refactor(ui): tokenize SessionTerminalView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:35:46 +02:00
mika kuns
0c0c73bc9e refactor(ui): tokenize AgentStripView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:35:22 +02:00
mika kuns
3d4a64a8fd fix(ui): use LineBrush for schedule flyout border and tokenize TaskRowView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:34:25 +02:00
mika kuns
bff15c9bf3 refactor(ui): tokenize DetailsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:33:46 +02:00
mika kuns
f40de4bbe0 refactor(ui): tokenize TasksIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:32:03 +02:00
mika kuns
e120b0fd70 refactor(ui): tokenize ListsIslandView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:31:39 +02:00
mika kuns
e8ce725897 refactor(ui): tokenize MainWindow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:31:00 +02:00
mika kuns
7a6bfbe1b4 refactor(ui): tokenize IslandStyles values and add shared modal styles 2026-05-30 16:28:47 +02:00
mika kuns
5a25818e3a feat(ui): set global Inter Tight font default on all windows 2026-05-30 16:24:00 +02:00
mika kuns
f0f8cd103d feat(ui): add named tint and hairline overlay brush tokens 2026-05-30 16:23:34 +02:00
mika kuns
d52f23f7c8 docs(ui): add UI normalization design spec and implementation plan 2026-05-30 16:22:00 +02:00
67 changed files with 8704 additions and 1461 deletions

2
.gitignore vendored
View File

@@ -45,6 +45,8 @@ artifacts/
# Avalonia / XAML designer
*.designer.cs
# ...but EF Core migration Designer files are real source and must be tracked
!**/Migrations/*.Designer.cs
# Project-specific
*.db

View File

@@ -161,11 +161,13 @@ Voraussetzung: funktionierendes Gitea-Release unter `git.kuns.dev/releases/Claud
## 4. Service-Deployment
### 4.1 Worker-Autostart als Per-User-Task ✅ (ersetzt Windows-Service)
- Der Worker läuft **nicht mehr als Windows-Service** (LocalSystem konnte die Claude-CLI-Auth des Users nicht sehen). Stattdessen: per-user **Logon-Scheduled-Task** „ClaudeDoWorker" (`schtasks /Create /XML`), läuft als angemeldeter User, versteckt, mit Restart-on-Failure.
- Worker ist `WinExe` (kein Konsolenfenster) + Serilog-File-Sink (`~/.todo-app/logs/worker-*.log`) + Single-Instance-Mutex.
- Installer migriert beim Update den alten Service automatisch weg (`sc stop`/`delete`) und registriert die Task; Uninstall entfernt Task + Worker-Prozess. App startet/neustartet den Worker als Prozess und sorgt beim Start dafür, dass er läuft.
- Implementiert 2026-05-29, getestet (Build + Unit-Tests grün), **manuelle E2E-Verifikation am Gerät ausstehend** (Update von 1.0.2-alpha → Task, Logoff/Logon-Autostart, Uninstall).
### 4.1 Worker-Autostart via Startup-Shortcut ✅ (ersetzt Scheduled Task + Windows-Service)
- Der Worker läuft als `WinExe` (kein Konsolenfenster) + Serilog-File-Sink (`~/.todo-app/logs/worker-*.log`) + Single-Instance-Mutex.
- Autostart über eine **Startup-Ordner-Verknüpfung** `ClaudeDo Worker.lnk` (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\`), die der Installer via `AutostartShortcut`/`ShortcutFactory` COM-Helper anlegt. Kein Scheduled Task, kein Windows-Service.
- `StartWorkerStep` startet den Worker per `Process.Start`; `StopWorkerStep` beendet ihn per prozessbasiertem Kill.
- Die App (`IslandsShellViewModel`) startet den Worker nicht selbst. Bei offline-Worker ~12s nach App-Start: einmaliges `WorkerConnectionModal` (Start Worker / Rerun Installer / Dismiss); Connection-Status-Pill in der Fußzeile ist ein Button zum erneuten Öffnen des Modals.
- `UninstallRunner` löscht die Startup-`.lnk`; migriert ältere Installs durch best-effort-Löschen des Legacy-Scheduled-Tasks „ClaudeDoWorker" und des Legacy-Windows-Service.
- **Manuelle E2E-Verifikation am Gerät ausstehend** (Logoff/Logon-Autostart, Update-Pfad, Uninstall).
### 4.2 Pfad-Auflösung absolut ✅
- `WorkerConfig.Load` expandiert `~`/`%USERPROFILE%` für alle Pfad-Felder.

View File

@@ -231,36 +231,21 @@ Beispiel: `feat(lager-app): add barcode scan retry logic`
DB-Zugriff via Microsoft.Data.Sqlite + Repository-Layer (`TaskRepository`, `ListRepository`). Git-Operationen (UI + Worker) über gemeinsamen `GitService` in `ClaudeDo.Data`. MVVM via CommunityToolkit.Mvvm.
## Worker als Windows-Service (Ziel-Deployment)
## Worker-Deployment (Autostart via Startup-Shortcut)
Initial läuft der Worker als Console-Prozess (lokales Dev-Setup). Im Endzustand soll er als **Windows-Service** automatisch starten.
Der Worker läuft als **WinExe** (kein Konsolenfenster) — kein Windows-Service, kein Scheduled Task.
**Code-seitig:**
- Paket `Microsoft.Extensions.Hosting.WindowsServices` referenzieren.
- In `Program.cs`: `builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker")`.
- Logging zusätzlich über `EventLog` (`builder.Logging.AddEventLog(...)`), damit Service-Fehler im Windows Event Viewer landen.
- Alle Pfade in `worker.config.json` **absolut** auflösen (`%USERPROFILE%` / `~` expandieren) — der Service-Working-Directory ist standardmäßig `C:\Windows\System32`.
- `StaleTaskRecovery` (siehe oben) sorgt nach Service-Restart automatisch für das Aufräumen hängender `running`-Tasks.
- Restart-Verhalten via `sc.exe failure`-Konfig oder beim Install.
**Autostart:** Der Installer legt eine Verknüpfung `ClaudeDo Worker.lnk` im Startup-Ordner des Users an (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\`). Dafür nutzt `ClaudeDo.Installer` den Helper `AutostartShortcut` (mit extrahiertem `ShortcutFactory` COM-Helper). Beim Windows-Logon startet Windows die Verknüpfung automatisch — ohne Elevated-Rechte und mit vollem Zugriff auf die `~/.claude/`-Session des Users.
**Install:**
- Veröffentlichen mit `dotnet publish -c Release -r win-x64 --self-contained false`.
- Service registrieren:
```cmd
sc.exe create ClaudeDoWorker binPath= "C:\Path\To\ClaudeDo.Worker.exe" start= auto
sc.exe failure ClaudeDoWorker reset= 60 actions= restart/5000/restart/10000/restart/30000
```
- Später optional: kleines `ClaudeDo.Installer`-Projekt (WiX oder MSIX), das das auch macht.
**Manueller Start (App-seitig):** Der Installer-Step `StartWorkerStep` startet den Worker beim Install/Update via `Process.Start` direkt. Die App (`IslandsShellViewModel`) startet den Worker **nicht** selbst. Stattdessen: ist der Worker ~12 Sekunden nach App-Start noch offline, erscheint einmalig ein `WorkerConnectionModal` mit drei Optionen (Start Worker / Rerun Installer / Dismiss). Der Connection-Status-Pill in der Fußzeile ist ein klickbarer Button, der das Modal auf Anfrage erneut öffnet.
**Auth-Konflikt mit "User-CLI-Session" beachten:**
Der Worker-Service läuft per Default unter `LocalSystem` — der hat **keinen Zugriff** auf die `~/.claude/`-Session des interaktiven Users, in der der CLI-Login liegt. Optionen:
**Stop/Uninstall:** `StopWorkerStep` beendet den Worker via prozessbasiertem Kill (kein `schtasks /End` mehr). `UninstallRunner` löscht die Startup-`.lnk`. Als Migrations-Schritt für ältere Installationen löscht der Uninstaller auch den Legacy-Scheduled-Task „ClaudeDoWorker" und den Legacy-Windows-Service (best-effort).
1. **Empfohlen:** Service unter dem **User-Account** laufen lassen (`sc.exe config ClaudeDoWorker obj= ".\<username>" password= "..."` oder via `services.msc` → "Log On As"). Dann greift die bestehende `claude login`-Session des Users. Voraussetzung: User-Account hat das Recht "Log on as a service".
2. **Fallback:** Wieder auf API-Key wechseln (`ANTHROPIC_API_KEY` als Umgebungsvariable des Service oder im `worker.config.json`). Dann ist der Service unabhängig vom User-Profil — verliert aber den Vorteil "kein Key-Handling".
**Logging:** Serilog-File-Sink nach `~/.todo-app/logs/worker-*.log`. Single-Instance-Mutex verhindert parallele Instanzen.
Entscheidung wird beim Service-Deployment getroffen, bleibt für die initiale Console-Variante irrelevant. Service-Modus erfordert keine Schema- oder API-Änderungen am Worker.
**Pfade:** `WorkerConfig.Load` expandiert `~`/`%USERPROFILE%` für alle Pfad-Felder.
**SignalR im Service-Modus:** Bindung bleibt `127.0.0.1:47821`. Da die UI auf demselben Rechner läuft, ist Loopback-Erreichbarkeit gegeben — Windows-Firewall greift bei Loopback nicht.
**SignalR:** Bindung bleibt `127.0.0.1:47821`. Da die UI auf demselben Rechner läuft, ist Loopback-Erreichbarkeit gegeben — Windows-Firewall greift bei Loopback nicht.
## Project-Layout (Monorepo)
@@ -319,4 +304,4 @@ Vorteil Monorepo: gemeinsames `schema.sql`, atomische Änderungen über UI+Worke
- Bulk-Discard alter Worktrees.
- Anzeige der ndjson-Message-Chronik im UI.
- Windows Job Objects für garantierten Child-Cleanup beim Worker-Crash.
- Installer-Projekt (`ClaudeDo.Installer`, WiX/MSIX), das den Service registriert + UI shortcut anlegt.
- Install-Skripte/Doku für manuelles Deployment ohne Installer.

View File

@@ -0,0 +1,36 @@
# UI Normalization — Visual Check
Run the app and walk each surface. Lane B intentionally shifted some values (12px→13px, 9px→10px, 16px→18px, off-palette colors folded to the palette), so small differences are expected — you're checking nothing looks *broken*.
## Global
- [ ] All text renders in **Inter Tight** (sans), not Segoe UI. Labels that were previously "off" (Settings field labels) now match.
- [ ] Mono text (chips, log lines, file paths, eyebrows, titlebar titles) still renders in JetBrains Mono.
## Main window
- [ ] Status-bar connection dot color: online = moss green, reconnecting = peat/amber, offline = blood red.
- [ ] Islands, task rows, chips, agent strips, terminal all look unchanged.
## Task row
- [ ] Schedule flyout (the date popup) renders with a visible border (was a broken/missing `BorderBrush` key — now `LineBrush`).
## Modals — now wrapped in ModalShell (check titlebar drag, ✕ close, footer buttons)
- [ ] **Settings** — titlebar "SETTINGS", drag works, ✕ closes, Cancel/Save footer. Tabs (General/Worktrees/Files/Prime Claude) intact.
- [ ] **List settings** — Delete (left) + Cancel/Save (right) footer; section panels intact.
- [ ] **Merge** — task summary + action buttons.
- [ ] **About** — version/data/logs/config labels.
- [ ] **Unfinished planning** — body text + primary action.
- [ ] **Repo import** — toolbar at top of body, repo list scrolls, footer.
- [ ] **Worktrees overview** — rows render; force-remove/phantom text is red (StatusError); state badge text legible. NOTE: window decorations changed to borderless (ModalShell draws the border) — confirm it still looks right.
- [ ] **Diff modal** — diff text mono, add/del colors, merge button in footer.
- [ ] **Conflict resolution** — now ModalShell; conflict list mono; error text red.
## Not wrapped in ModalShell (intentional — distinct chrome)
- [ ] **Worktree modal** (the big 1100×720 acrylic-blur diff window) — unchanged look, fonts slightly normalized.
- [ ] **Planning diff view** (embedded) — diff renders, mono font, warning text red.
## Date picker
- [ ] Selected day: accent background with light text (was hardcoded white → TextBrush).
## If something looks wrong
- Font/size off → check the snap mapping in `2026-05-30-ui-normalization.md` (11→Mono=11, 12→Body=13).
- A modal's layout broke → that modal's body may have coupled to the old Grid rows; revert just that file's ModalShell wrap and keep only the token changes (the fallback noted in the plan).

View File

@@ -0,0 +1,473 @@
# UI Normalization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the design tokens the single source of truth for every visual value in the Avalonia UI, remove duplicated styles, and add a reusable `ModalShell` control for the copy-pasted modal chrome.
**Architecture:** Establish global control defaults in `App.axaml`, expand/repoint brushes in `Tokens.axaml`, promote shared styles into `IslandStyles.axaml`, then mechanically migrate every view to reference tokens (snapping stray values to the nearest token per "lane B"). Off-palette colors fold into the existing palette. A new `ModalShell` templated control replaces the per-modal titlebar/border/footer markup.
**Tech Stack:** .NET 8, Avalonia 12 (Fluent theme, dark variant), compiled XAML (`x:DataType`), CommunityToolkit.Mvvm.
**Verification model:** There are no unit tests for XAML. The "test" for every task is a clean build:
- `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` (compiles Ui + Data; validates all StaticResource keys and compiled bindings)
Build with the `.csproj` directly — `.slnx` requires .NET 9 and will fail on this machine (.NET 8).
**Normalization rules (apply everywhere unless a task says otherwise):**
Font sizes — replace every `FontSize="N"` literal with the token whose value it snaps to:
| literal | token |
|---|---|
| 9 | `{StaticResource FontSizeEyebrow}` (10) |
| 10 | `{StaticResource FontSizeEyebrow}` (10) |
| 11 | `{StaticResource FontSizeMono}` (11) |
| 12 | `{StaticResource FontSizeBody}` (13) |
| 13 | `{StaticResource FontSizeBody}` (13) |
| 14 | `{StaticResource FontSizeTaskTitle}` (14) |
| 16 | `{StaticResource FontSizeH3}` (18) |
| 18 | `{StaticResource FontSizeH3}` (18) |
| 24 | `{StaticResource FontSizeH2}` (24) |
| 32 | `{StaticResource FontSizeH1}` (32) |
Spacing — modal body padding literals `16` and `20` snap to `18`; keep other axis values mapped to the nearest of SpaceXs=4/SpaceSm=8/SpaceMd=12/SpaceLg=14/SpaceXl=18/Space2Xl=24. Leave values that already equal a token as plain numbers (do **not** churn every margin into a resource ref — only modal body padding is standardized).
Corner radius — `4``6`; TextBox inputs use `8`.
Colors — fold off-palette to palette:
| literal / named | replacement |
|---|---|
| `#4CAF50` (online dot) | `{DynamicResource StatusRunningBrush}` |
| `#FFA726` (reconnecting dot) | `{DynamicResource StatusReviewBrush}` |
| `#EF5350` (offline / phantom) | `{DynamicResource StatusErrorBrush}` |
| `OrangeRed`, `Orange` | `{DynamicResource BloodBrush}` |
| `White` (badge / danger text) | `{DynamicResource TextBrush}` |
| `White` (on accent primary button) | `{DynamicResource DeepBrush}` |
| `#FF080C0B` (terminal bg) | `{DynamicResource VoidBrush}` |
| `#0DFFFFFF` (island hairline) | `{DynamicResource HairlineOverlayBrush}` |
---
## Phase 1 — Foundation
### Task 1: Add new brushes & repoint badges in Tokens.axaml
**Files:**
- Modify: `src/ClaudeDo.Ui/Design/Tokens.axaml`
- [ ] **Step 1: Add named tint, hairline brushes**
In the BRUSHES section (after the Status*Brush block ending ~line 85), add:
```xml
<!-- Subtle white overlay (island hairline border) -->
<SolidColorBrush x:Key="HairlineOverlayBrush" Color="#0DFFFFFF" />
<!-- Status tints (12% fill / 30% border of the status hue) — reused by chips & agent strips -->
<SolidColorBrush x:Key="RunningTintBrush" Color="#1F7C9166" />
<SolidColorBrush x:Key="RunningTintBorderBrush" Color="#4C7C9166" />
<SolidColorBrush x:Key="ReviewTintBrush" Color="#1FD4A574" />
<SolidColorBrush x:Key="ReviewTintBorderBrush" Color="#4CD4A574" />
<SolidColorBrush x:Key="ErrorTintBrush" Color="#1FC87060" />
<SolidColorBrush x:Key="ErrorTintBorderBrush" Color="#4CC87060" />
<SolidColorBrush x:Key="QueuedTintBrush" Color="#1F8B9D7A" />
<SolidColorBrush x:Key="QueuedTintBorderBrush" Color="#4C8B9D7A" />
```
- [ ] **Step 2: Build to verify tokens parse**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS (no errors).
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeDo.Ui/Design/Tokens.axaml
git commit -m "feat(ui): add named tint and hairline overlay brush tokens"
```
---
### Task 2: Global control defaults in App.axaml
**Files:**
- Modify: `src/ClaudeDo.App/App.axaml`
- [ ] **Step 1: Add Window default style**
Inside `<Application.Styles>`, after `<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />` and before the ListBoxItem styles, add:
```xml
<!-- Global defaults: every Window inherits Inter Tight + body size.
Controls that need mono opt in via their own class/style. -->
<Style Selector="Window">
<Setter Property="FontFamily" Value="{DynamicResource SansFont}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeBody}" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
</Style>
```
(FontFamily/FontSize/Foreground are inherited properties in Avalonia, so setting them on the Window root propagates to all descendant text controls.)
- [ ] **Step 2: Build**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS.
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeDo.App/App.axaml
git commit -m "feat(ui): set global Inter Tight font default on all windows"
```
---
### Task 3: Promote shared styles into IslandStyles.axaml
**Files:**
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
- [ ] **Step 1: Add shared modal styles**
At the end of the `<Styles>` element (before the closing `</Styles>` at line ~901), add:
```xml
<!-- ============================================================ -->
<!-- SHARED MODAL STYLES (promoted from per-modal Window.Styles) -->
<!-- ============================================================ -->
<Style Selector="TextBlock.field-label">
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style Selector="TextBlock.path-mono">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<!-- Standalone modal action buttons (not the .btn family) -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource BloodBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
```
Note: `TextBlock.section-label` already exists at line ~864 — do NOT re-add it.
- [ ] **Step 2: Replace hardcoded values inside existing IslandStyles rules**
Apply the normalization rules to the existing style setters in this file:
- Every `FontSize="N"` setter → the snapped token ref (table above). Specific lines: 149 (10→FontSizeEyebrow), 206 (11→FontSizeMono), 252 (13→FontSizeBody), 397 (11→FontSizeMono), 453 (9→FontSizeEyebrow), 475 (10→FontSizeEyebrow), 483 (10→FontSizeEyebrow), 556 (12→FontSizeBody), 573 (9→FontSizeEyebrow), 597 (12→FontSizeBody), 622 (10→FontSizeEyebrow), 638 (12→FontSizeBody), 697 (14→FontSizeTaskTitle), 771 (10→FontSizeEyebrow), 783 (10→FontSizeEyebrow), 788 (10→FontSizeEyebrow), 819 (11→FontSizeMono), 867 (10→FontSizeEyebrow), 884 (9→FontSizeEyebrow).
- Chip tint backgrounds/borders → named brushes:
- line 155/156 `#1F7C9166`/`#4C7C9166``{StaticResource RunningTintBrush}`/`{StaticResource RunningTintBorderBrush}`
- 163/164 review tints → `ReviewTintBrush`/`ReviewTintBorderBrush`
- 171/172 error tints → `ErrorTintBrush`/`ErrorTintBorderBrush`
- 179/180 queued tints → `QueuedTintBrush`/`QueuedTintBorderBrush`
- agent-strip tints at 361/362 (`#147C9166`/`#4C7C9166`), 365/366, 368/369, 374/375 → the matching `*TintBrush`/`*TintBorderBrush` (snap the `#14` alpha to the shared `#1F` tint).
- line 123 `#0DFFFFFF``{StaticResource HairlineOverlayBrush}`.
- line 389 & 810 `#FF080C0B``{StaticResource VoidBrush}`.
- line 887 badge `White``{StaticResource TextBrush}`.
- Badge brushes at lines 88-90: replace the three `<SolidColorBrush>` definitions with palette refs:
```xml
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
```
- Corner radius `4` setters (447 live-chip, 813 task-live-tail `5`→leave, badges 878 `3`→leave) → only snap `4``6` where it appears as `CornerRadius="4"` on live-chip (447) and kbd (614) and badge tints. Leave `3` and `5` as-is (no nearby token; they're intentional micro-radii). NOTE: if unsure, leave radius alone — radius churn is lowest priority.
- [ ] **Step 3: Build**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS.
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml
git commit -m "refactor(ui): tokenize IslandStyles values and add shared modal styles"
```
---
## Phase 2 — Per-view token migration (independent; parallelizable)
For each task: open the file, apply the **normalization rules** (font/color/spacing/radius tables at top). Remove any local `Window.Styles` block that only redefines `section-label`, `field-label`, `path-mono`, `Button.primary`, or `Button.danger` (now shared from IslandStyles). Keep local styles that are genuinely unique to that view. After each file, build and commit.
Each task ends with:
- Build: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` → PASS
- Commit: `git add <file> && git commit -m "refactor(ui): tokenize <view>"`
### Task 4: MainWindow.axaml
- Snap all `FontSize` literals (lines ~46,52,59,67,112,136,209,222,231).
- Status dots: `#4CAF50``StatusRunningBrush`, `#FFA726``StatusReviewBrush`, `#EF5350``StatusErrorBrush` (lines ~200,203,205).
### Task 5: Islands — ListsIslandView.axaml, TasksIslandView.axaml
- ListsIslandView: snap FontSize (18,10,12 at lines ~18,49,57,58,59); username TextBlock (~57) gets no explicit FontFamily (inherits SansFont now — correct, leave it).
- TasksIslandView: snap FontSize (24,11 at ~15,19).
### Task 6: DetailsIslandView.axaml
- Snap all FontSize (10,14,11,10,13,12 at lines ~54,57,92,114,138,142,199,269).
- `OrangeRed``BloodBrush` (~154).
- TextBox `CornerRadius="6"``8` (~172,274). TextBox `Padding="8"` leave.
- Remove any redundant inline label styles superseded by shared `field-label`.
### Task 7: TaskRowView.axaml (includes the BorderBrush bug fix)
- Snap FontSize (10,14 at ~85,103).
- **Bug fix:** `BorderBrush="{DynamicResource BorderBrush}"``{DynamicResource LineBrush}` (the schedule-flyout border, ~line 188/222). `BorderBrush` is not a defined key.
- Schedule flyout: title/labels inherit SansFont now (leave unset).
### Task 8: AgentStripView.axaml, SessionTerminalView.axaml
- AgentStrip: snap FontSize (10,9 at ~22,29,73,78); commit chip radius `4``6` (~102).
- SessionTerminal: snap FontSize (10,11 at ~17,69).
### Task 9: ThemedDatePicker.axaml
- Snap any FontSize literals; popup border `CornerRadius="10"` → leave (10 = ChipCornerRadius value, acceptable) OR `{StaticResource ChipCornerRadius}`. Tokenize colors if any literals present.
---
## Phase 3 — ModalShell control
### Task 10: Create ModalShell control
**Files:**
- Create: `src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml.cs`
- Create: `src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml`
- [ ] **Step 1: Write the code-behind (templated control)**
`ModalShell.axaml.cs`:
```csharp
using System;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace ClaudeDo.Ui.Views.Controls;
/// <summary>Reusable modal chrome: titlebar (drag + close) wrapping a body and optional footer.</summary>
public class ModalShell : ContentControl
{
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<ModalShell, string?>(nameof(Title));
public static readonly StyledProperty<object?> FooterProperty =
AvaloniaProperty.Register<ModalShell, object?>(nameof(Footer));
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
AvaloniaProperty.Register<ModalShell, ICommand?>(nameof(CloseCommand));
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<Border>("PART_TitleBar") is { } bar)
bar.PointerPressed += OnTitleBarPressed;
}
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed
&& VisualRoot is Window w)
w.BeginMoveDrag(e);
}
}
```
- [ ] **Step 2: Write the ControlTheme**
`ModalShell.axaml`:
```xml
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls">
<ControlTheme x:Key="{x:Type ctl:ModalShell}" TargetType="ctl:ModalShell">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ModalCornerRadius}"
ClipToBounds="True">
<DockPanel>
<!-- Title bar -->
<Border Name="PART_TitleBar" DockPanel.Dock="Top" Height="36"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="{TemplateBinding Title}"
FontFamily="{DynamicResource MonoFont}"
FontSize="{DynamicResource FontSizeMono}"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
FontSize="{DynamicResource FontSizeBody}"
Command="{TemplateBinding CloseCommand}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- Footer (optional) -->
<Border Name="PART_Footer" DockPanel.Dock="Bottom"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
IsVisible="{TemplateBinding Footer, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentPresenter Content="{TemplateBinding Footer}" Margin="16,8"/>
</Border>
<!-- Body -->
<ContentPresenter Content="{TemplateBinding Content}"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
```
- [ ] **Step 3: Register the ControlTheme**
In `src/ClaudeDo.App/App.axaml`, inside `<ResourceDictionary.MergedDictionaries>` (after the Tokens include), add:
```xml
<ResourceInclude Source="avares://ClaudeDo.Ui/Views/Controls/ModalShell.axaml" />
```
- [ ] **Step 4: Build**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml.cs src/ClaudeDo.App/App.axaml
git commit -m "feat(ui): add reusable ModalShell control"
```
---
### Task 11: Migrate SettingsModalView to ModalShell (reference migration)
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
- Modify (if needed): `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs`
- [ ] **Step 1: Replace chrome with ModalShell**
- Add namespace if missing: `xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"` (already present).
- Remove the local `Window.Styles` entries for `section-label`, `field-label`, `path-mono`, `Button.danger`, `Button.primary` (now shared). Keep any genuinely unique styles.
- Replace the outer `<Border>...<Grid RowDefinitions="36,*,52">` structure with:
```xml
<ctl:ModalShell Title="SETTINGS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- existing DockPanel body (tabs + validation strip) goes here unchanged -->
</ctl:ModalShell>
```
- The body is the existing `<DockPanel Grid.Row="1">` content minus `Grid.Row`.
- Snap remaining FontSize literals in the body per the rules.
- [ ] **Step 2: Remove obsolete drag handler if now unused**
If `TitleBar_PointerPressed` in `SettingsModalView.axaml.cs` is no longer referenced (ModalShell handles dragging), delete the method and the `x:Name="TitleBar"`/`PointerPressed` wiring. If the build complains about an unused handler, that's the signal.
- [ ] **Step 3: Build**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: PASS.
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs
git commit -m "refactor(ui): migrate SettingsModal to ModalShell"
```
---
### Task 12: Migrate remaining modals to ModalShell
Repeat the Task 11 pattern for each modal below. One commit per file. Each: swap chrome → `ModalShell`, lift action buttons into `ModalShell.Footer`, drop local duplicate styles, delete now-unused `*_PointerPressed` drag handlers, snap FontSize/colors per rules, build, commit.
- [ ] **12a:** `ListSettingsModalView.axaml` (+ `.axaml.cs`)
- [ ] **12b:** `MergeModalView.axaml` (+ `.axaml.cs`)
- [ ] **12c:** `AboutModalView.axaml` (+ `.axaml.cs`) — labels inherit SansFont now.
- [ ] **12d:** `UnfinishedPlanningModalView.axaml` (+ `.axaml.cs`)
- [ ] **12e:** `RepoImportModalView.axaml` (+ `.axaml.cs`)
- [ ] **12f:** `WorktreesOverviewModalView.axaml` (+ `.axaml.cs`) — also fold `Border.wt-row` to reuse `task-row` if trivial; snap FontSize; `#EF5350``StatusErrorBrush`; `White` badge text→`TextBrush`.
Each ends with build PASS + `git commit -m "refactor(ui): migrate <Modal> to ModalShell"`.
---
### Task 13: DiffModalView, PlanningDiffView, ConflictResolutionView (Static→Dynamic + chrome)
These three currently use `StaticResource` for token lookups. Migrate chrome to `ModalShell` where they are full windows, and convert token references.
- [ ] **Step 1: Convert resource references**
In each of `DiffModalView.axaml`, `PlanningDiffView.axaml`, `ConflictResolutionView.axaml`: change every `{StaticResource <Brush/Token>}` used in an **element attribute** to `{DynamicResource ...}`. Leave `{StaticResource ...}` inside `<Style>`/`Setter` blocks (Avalonia styles resolve StaticResource fine and DynamicResource in setters is discouraged).
- [ ] **Step 2: Apply normalization rules**
- Snap FontSize literals.
- `Consolas,Menlo,monospace` raw font (PlanningDiffView ~98, ConflictResolution ~47) → `{DynamicResource MonoFont}`.
- `Orange`/`OrangeRed``{DynamicResource BloodBrush}`.
- DiffModal tints `#1A4A6B4A`/`#1AC87060``{DynamicResource RunningTintBrush}`/`{DynamicResource ErrorTintBrush}`.
- Migrate window chrome to `ModalShell` if the file is a Window with the titlebar/footer pattern (DiffModalView, ConflictResolutionView). PlanningDiffView is an embedded view — only convert resources + fonts, no ModalShell.
- [ ] **Step 3: Build + commit (one per file)**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` → PASS
Commit: `git commit -m "refactor(ui): tokenize and dynamic-ize <view>"`
---
## Phase 4 — Final verification
### Task 14: Full build + visual checklist
- [ ] **Step 1: Build both projects**
Run:
```bash
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
```
Expected: both PASS.
- [ ] **Step 2: Grep for stragglers**
Confirm no remaining hardcoded values slipped through:
- `FontSize="` with a numeric literal in any `Views/**/*.axaml` (should be near-zero; only token refs remain).
- Off-palette hex (`#4CAF50`, `#FFA726`, `#EF5350`, `#FF080C0B`, `OrangeRed`, `Orange`) — should be zero.
- [ ] **Step 3: Produce the human visual-check checklist**
Write a short checklist (`docs/superpowers/plans/2026-05-30-ui-normalization-visualcheck.md`) listing each view/modal and what to eyeball (font looks like Inter Tight, status dots correct color, modal titlebars/footers intact, badges distinguishable, diff/planning views render). This is the regression gate the user runs by launching the app.
---
## Self-Review notes
- **Spec coverage:** global defaults (T2), token source-of-truth fonts/spacing/radius (rules + T3T13), color fold (T1,T3,T4,T6,T12,T13), shared styles (T3), ModalShell (T10T13), bug fixes — BorderBrush (T7), Static→Dynamic (T13). All spec sections mapped.
- **Risk note:** ModalShell migration (T11T13) is the highest-risk part because each modal's body layout differs. Tasks are per-file so a failure is isolated. If a modal's body has tight coupling to the old Grid rows, keeping that modal's hand-rolled chrome (and only tokenizing it) is an acceptable fallback — note it in the commit.
- **Line numbers** are from the pre-change audit and may drift as edits land; treat them as guides, locate by content.

View File

@@ -0,0 +1,829 @@
# Worker Lifecycle Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the worker owned by a single external mechanism (a per-user Startup-folder shortcut in production), stop the App from auto-spawning its own worker, and show an actionable prompt when the App can't connect.
**Architecture:** Installer creates a `.lnk` in the Windows Startup folder instead of a Scheduled Task (migrating existing installs by deleting the old task). The App's `IslandsShellViewModel` drops `EnsureWorkerRunningAsync` and instead runs a one-shot grace timer that opens a `WorkerConnectionModal` (Start Worker / Rerun Installer / Dismiss) if still offline; the footer status pill becomes a button that reopens it.
**Tech Stack:** .NET 8, WPF installer (COM `IShellLink` for shortcuts), Avalonia + CommunityToolkit.Mvvm UI, xUnit.
---
## File Structure
**Installer (`src/ClaudeDo.Installer`)**
- Create: `Core/ShortcutFactory.cs` — shared `IShellLink` COM helper (`CreateShortcut`).
- Create: `Core/AutostartShortcut.cs` — install/remove the worker Startup-folder `.lnk`.
- Modify: `Steps/CreateShortcutsStep.cs` — use `ShortcutFactory`, drop embedded COM.
- Modify: `Steps/RegisterAutostartStep.cs` — Startup shortcut + legacy-task delete (no more task XML).
- Modify: `Steps/StartWorkerStep.cs``Process.Start` instead of `schtasks /Run`.
- Modify: `Steps/StopWorkerStep.cs` — drop `schtasks /End`.
- Modify: `Core/UninstallRunner.cs` — remove the Startup `.lnk`.
- Delete: `Core/ScheduledTaskXml.cs` (and its test).
**App (`src/ClaudeDo.Ui`)**
- Create: `ViewModels/Modals/WorkerConnectionModalViewModel.cs`.
- Create: `Views/Modals/WorkerConnectionModalView.axaml` (+ `.axaml.cs`).
- Modify: `ViewModels/IslandsShellViewModel.cs` — remove auto-spawn; add hook, command, grace timer, decision gate.
- Modify: `Views/MainWindow.axaml.cs` — wire the new modal.
- Modify: `Views/MainWindow.axaml` — clickable status pill.
**Tests**
- Modify: `tests/ClaudeDo.Installer.Tests/` — delete `ScheduledTaskXmlTests.cs`; add `ShortcutFactoryTests.cs`, `AutostartShortcutTests.cs`.
- Add: `tests/ClaudeDo.Ui.Tests/ConnectionPromptGateTests.cs`.
---
## Task 1: ShortcutFactory (shared COM helper)
**Files:**
- Create: `src/ClaudeDo.Installer/Core/ShortcutFactory.cs`
- Modify: `src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs`
- Test: `tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs`
- [ ] **Step 1: Write the failing test**
`tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs`:
```csharp
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class ShortcutFactoryTests
{
[Fact]
public void CreateShortcut_writes_lnk_file()
{
var dir = Path.Combine(Path.GetTempPath(), "cdshortcut-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
try
{
var target = Path.Combine(dir, "fake.exe");
File.WriteAllText(target, "");
var lnk = Path.Combine(dir, "x.lnk");
ShortcutFactory.CreateShortcut(lnk, target, dir, "desc");
Assert.True(File.Exists(lnk));
}
finally { Directory.Delete(dir, recursive: true); }
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter ShortcutFactoryTests`
Expected: FAIL — `ShortcutFactory` does not exist (compile error).
- [ ] **Step 3: Create `ShortcutFactory` (move COM interop out of `CreateShortcutsStep`)**
`src/ClaudeDo.Installer/Core/ShortcutFactory.cs`:
```csharp
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace ClaudeDo.Installer.Core;
public static class ShortcutFactory
{
public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
{
var link = (IShellLink)new ShellLink();
link.SetPath(targetPath);
link.SetWorkingDirectory(workingDir);
link.SetDescription(description);
link.SetIconLocation(targetPath, 0);
var file = (IPersistFile)link;
file.Save(shortcutPath, false);
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink { }
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLink
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
}
```
- [ ] **Step 4: Replace the embedded COM in `CreateShortcutsStep` with the helper**
In `src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs`: delete the private `CreateShortcut` method and the entire `#region COM Interop for IShellLink` block (lines 47-90), remove the now-unused `using System.Runtime.InteropServices;`, `using System.Runtime.InteropServices.ComTypes;`, and `using System.Text;`. Replace the two `CreateShortcut(...)` call sites with `ShortcutFactory.CreateShortcut(...)`:
```csharp
ShortcutFactory.CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager");
```
```csharp
ShortcutFactory.CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager");
```
- [ ] **Step 5: Run tests to verify they pass**
Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter ShortcutFactoryTests`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add src/ClaudeDo.Installer/Core/ShortcutFactory.cs src/ClaudeDo.Installer/Steps/CreateShortcutsStep.cs tests/ClaudeDo.Installer.Tests/ShortcutFactoryTests.cs
git commit -m "refactor(installer): extract ShortcutFactory COM helper"
```
---
## Task 2: AutostartShortcut helper
**Files:**
- Create: `src/ClaudeDo.Installer/Core/AutostartShortcut.cs`
- Test: `tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs`
- [ ] **Step 1: Write the failing tests**
`tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs`:
```csharp
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class AutostartShortcutTests
{
private static string TempDir()
{
var dir = Path.Combine(Path.GetTempPath(), "cdautostart-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}
[Fact]
public void Install_creates_lnk_with_expected_name()
{
var startup = TempDir();
var workerDir = TempDir();
try
{
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
File.WriteAllText(workerExe, "");
AutostartShortcut.Install(startup, workerExe);
Assert.True(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
}
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
}
[Fact]
public void Remove_deletes_existing_lnk()
{
var startup = TempDir();
var workerDir = TempDir();
try
{
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
File.WriteAllText(workerExe, "");
AutostartShortcut.Install(startup, workerExe);
AutostartShortcut.Remove(startup);
Assert.False(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
}
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
}
[Fact]
public void Remove_is_noop_when_missing()
{
var startup = TempDir();
try { AutostartShortcut.Remove(startup); } // must not throw
finally { Directory.Delete(startup, true); }
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter AutostartShortcutTests`
Expected: FAIL — `AutostartShortcut` does not exist.
- [ ] **Step 3: Create `AutostartShortcut`**
`src/ClaudeDo.Installer/Core/AutostartShortcut.cs`:
```csharp
using System.IO;
namespace ClaudeDo.Installer.Core;
public static class AutostartShortcut
{
public const string FileName = "ClaudeDo Worker.lnk";
public static string DefaultStartupDir =>
Environment.GetFolderPath(Environment.SpecialFolder.Startup);
public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName);
public static void Install(string startupDir, string workerExe)
{
Directory.CreateDirectory(startupDir);
var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir;
ShortcutFactory.CreateShortcut(PathIn(startupDir), workerExe, workingDir, "ClaudeDo background worker");
}
public static void Remove(string startupDir)
{
var path = PathIn(startupDir);
if (File.Exists(path)) File.Delete(path);
}
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `dotnet test tests/ClaudeDo.Installer.Tests --filter AutostartShortcutTests`
Expected: PASS (3 tests).
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Installer/Core/AutostartShortcut.cs tests/ClaudeDo.Installer.Tests/AutostartShortcutTests.cs
git commit -m "feat(installer): add AutostartShortcut helper for Startup-folder lnk"
```
---
## Task 3: RegisterAutostartStep → Startup shortcut + task migration
**Files:**
- Modify: `src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs`
- Delete: `src/ClaudeDo.Installer/Core/ScheduledTaskXml.cs`
- Delete: `tests/ClaudeDo.Installer.Tests/ScheduledTaskXmlTests.cs`
- [ ] **Step 1: Replace the step body**
Rewrite `src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs` to:
```csharp
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class RegisterAutostartStep : IInstallStep
{
public const string LegacyTaskName = "ClaudeDoWorker";
private const string LegacyServiceName = "ClaudeDoWorker";
public string Name => "Register Autostart";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe");
if (!File.Exists(workerExe))
return StepResult.Fail($"Worker executable not found: {workerExe}");
// 1) Migrate away the legacy Windows service if present.
progress.Report("Checking for legacy worker service...");
var (queryExit, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
if (queryExit == 0)
{
progress.Report("Removing legacy worker service...");
await ProcessRunner.RunAsync("sc.exe", $"stop {LegacyServiceName}", null, progress, ct);
await ProcessRunner.RunAsync("sc.exe", $"delete {LegacyServiceName}", null, progress, ct);
for (var i = 0; i < 30; i++)
{
ct.ThrowIfCancellationRequested();
var (q, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
if (q != 0) break;
await Task.Delay(1000, ct);
}
}
// 2) Migrate away the legacy logon scheduled task if present (best-effort).
progress.Report("Removing legacy logon task...");
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
// 3) Register per-user autostart via a Startup-folder shortcut.
progress.Report("Creating Startup shortcut...");
try
{
AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
}
catch (Exception ex)
{
return StepResult.Fail($"Failed to create Startup shortcut: {ex.Message}");
}
return StepResult.Ok();
}
}
```
- [ ] **Step 2: Delete the obsolete scheduled-task code and its test**
Run:
```bash
git rm src/ClaudeDo.Installer/Core/ScheduledTaskXml.cs tests/ClaudeDo.Installer.Tests/ScheduledTaskXmlTests.cs
```
- [ ] **Step 3: Build the installer to verify it compiles**
Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj`
Expected: Build succeeded. (If `RegisterAutostartStep.TaskName` was referenced elsewhere, the build will flag it — Task 4 and Task 5 update those references; if the build fails only there, proceed to those tasks before re-running.)
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs
git commit -m "feat(installer): register autostart via Startup shortcut, drop scheduled task"
```
---
## Task 4: StartWorkerStep + StopWorkerStep
**Files:**
- Modify: `src/ClaudeDo.Installer/Steps/StartWorkerStep.cs`
- Modify: `src/ClaudeDo.Installer/Steps/StopWorkerStep.cs`
- [ ] **Step 1: Rewrite `StartWorkerStep` to launch the exe directly**
`src/ClaudeDo.Installer/Steps/StartWorkerStep.cs`:
```csharp
using System.Diagnostics;
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class StartWorkerStep : IInstallStep
{
public string Name => "Start Worker";
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe");
if (!File.Exists(workerExe))
return Task.FromResult(StepResult.Fail($"Worker executable not found: {workerExe}"));
progress.Report("Starting worker...");
try
{
Process.Start(new ProcessStartInfo(workerExe) { UseShellExecute = true });
return Task.FromResult(StepResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(StepResult.Fail($"Failed to start worker: {ex.Message}"));
}
}
}
```
- [ ] **Step 2: Drop the `schtasks /End` call in `StopWorkerStep`**
In `src/ClaudeDo.Installer/Steps/StopWorkerStep.cs`, remove these two lines (the task no longer exists; the process kill below is the real stop):
```csharp
progress.Report("Stopping worker task (if running)...");
await ProcessRunner.RunAsync("schtasks.exe", $"/End /TN \"{TaskName}\"", null, progress, ct);
```
Keep the `public const string TaskName = "ClaudeDoWorker";` line — `UninstallRunner` still references it for legacy-task cleanup (Task 5). The method keeps its `async` modifier (it still has `await Task.CompletedTask;`).
- [ ] **Step 3: Build to verify it compiles**
Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj`
Expected: Build succeeded.
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Installer/Steps/StartWorkerStep.cs src/ClaudeDo.Installer/Steps/StopWorkerStep.cs
git commit -m "feat(installer): start worker via Process.Start, drop schtasks stop"
```
---
## Task 5: UninstallRunner removes the Startup shortcut
**Files:**
- Modify: `src/ClaudeDo.Installer/Core/UninstallRunner.cs`
- [ ] **Step 1: Add Startup `.lnk` removal**
In `src/ClaudeDo.Installer/Core/UninstallRunner.cs`, the shortcut-removal block (step 4, around lines 53-60) currently removes the Desktop and Start Menu `.lnk`s. Add the Startup shortcut removal right after them:
```csharp
// 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest).
progress.Report("Removing shortcuts...");
TryDeleteFile(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
"ClaudeDo.lnk"));
TryDeleteFile(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs", "ClaudeDo.lnk"));
TryDeleteFile(AutostartShortcut.PathIn(AutostartShortcut.DefaultStartupDir));
```
The existing `schtasks /Delete /TN "{StopWorkerStep.TaskName}" /F` line (step 3) stays — it cleans up the legacy task on machines that still have it.
- [ ] **Step 2: Build to verify it compiles**
Run: `dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj`
Expected: Build succeeded.
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeDo.Installer/Core/UninstallRunner.cs
git commit -m "feat(installer): remove Startup worker shortcut on uninstall"
```
---
## Task 6: App stops auto-spawning the worker
**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
- [ ] **Step 1: Remove the auto-spawn call**
In `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`, delete this line from the constructor (line 224):
```csharp
_ = EnsureWorkerRunningAsync();
```
- [ ] **Step 2: Remove the `EnsureWorkerRunningAsync` method and its flag**
Delete the `_ensureRunningAttempted` field (line 308) and the whole `EnsureWorkerRunningAsync` method (lines 310-320):
```csharp
private bool _ensureRunningAttempted;
private async Task EnsureWorkerRunningAsync()
{
if (_ensureRunningAttempted) return;
_ensureRunningAttempted = true;
await Task.Delay(TimeSpan.FromSeconds(4));
if (Worker?.IsConnected == true) return;
var exe = _workerLocator.Find();
if (exe is null) return;
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true }); }
catch { /* logon task is the primary mechanism; this is a convenience */ }
}
```
Keep `RestartWorkerAsync` / `RestartWorkerService` (still used by the existing Restart button). `_workerLocator` stays in use (RestartWorkerService + Task 8).
- [ ] **Step 3: Build to verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded (no remaining references to `EnsureWorkerRunningAsync`).
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
git commit -m "refactor(ui): stop auto-spawning the worker on app start"
```
---
## Task 7: WorkerConnectionModal (VM + View)
**Files:**
- Create: `src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs`
- Create: `src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml`
- Create: `src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml.cs`
- [ ] **Step 1: Create the ViewModel**
`src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs`:
```csharp
using System;
using System.Diagnostics;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals;
public sealed partial class WorkerConnectionModalViewModel : ViewModelBase
{
private readonly WorkerLocator _workerLocator;
private readonly InstallerLocator _installerLocator;
public WorkerConnectionModalViewModel(WorkerLocator workerLocator, InstallerLocator installerLocator)
{
_workerLocator = workerLocator;
_installerLocator = installerLocator;
}
public Action? CloseAction { get; set; }
[RelayCommand] private void Close() => CloseAction?.Invoke();
[RelayCommand]
private void StartWorker()
{
var exe = _workerLocator.Find();
if (exe is null) return;
try { Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); }
catch { /* nothing useful to show */ }
CloseAction?.Invoke();
}
[RelayCommand]
private void RerunInstaller()
{
var path = _installerLocator.Find();
if (path is null) return;
try
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
Environment.Exit(0);
}
catch { /* nothing useful to show */ }
}
}
```
- [ ] **Step 2: Create the View (mirrors `AboutModalView` + `ModalShell`)**
`src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml`:
```xml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.WorkerConnectionModalView"
x:DataType="vm:WorkerConnectionModalViewModel"
Title="Worker not reachable"
Width="520" Height="240"
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="WORKER NOT REACHABLE" CloseCommand="{Binding CloseCommand}">
<Grid RowDefinitions="*,Auto" Margin="20,16">
<TextBlock Grid.Row="0" Classes="meta" TextWrapping="Wrap"
Text="ClaudeDo can't reach the background worker. It is normally started automatically at logon. You can start it now, or reinstall if the problem persists."/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,16,0,0">
<Button Classes="btn" Content="Dismiss" Command="{Binding CloseCommand}"/>
<Button Classes="btn" Content="Rerun Installer" Command="{Binding RerunInstallerCommand}"/>
<Button Classes="btn primary" Content="Start Worker" Command="{Binding StartWorkerCommand}"/>
</StackPanel>
</Grid>
</ctl:ModalShell>
</Window>
```
`src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml.cs`:
```csharp
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ClaudeDo.Ui.Views.Modals;
public partial class WorkerConnectionModalView : Window
{
public WorkerConnectionModalView()
{
InitializeComponent();
}
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
```
- [ ] **Step 3: Build to verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded.
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml src/ClaudeDo.Ui/Views/Modals/WorkerConnectionModalView.axaml.cs
git commit -m "feat(ui): add worker connection help modal"
```
---
## Task 8: Shell hook, command, grace timer + decision gate
**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
- Test: `tests/ClaudeDo.Ui.Tests/ConnectionPromptGateTests.cs`
- [ ] **Step 1: Write the failing test for the decision gate**
`tests/ClaudeDo.Ui.Tests/ConnectionPromptGateTests.cs`:
```csharp
using ClaudeDo.Ui.ViewModels;
using Xunit;
namespace ClaudeDo.Ui.Tests;
public class ConnectionPromptGateTests
{
[Fact]
public void Shows_once_when_offline()
{
var vm = new IslandsShellViewModel();
Assert.True(vm.DecideShowConnectionPrompt(isOffline: true));
Assert.False(vm.DecideShowConnectionPrompt(isOffline: true)); // not a second time
}
[Fact]
public void Does_not_show_when_connected_before_grace()
{
var vm = new IslandsShellViewModel();
Assert.False(vm.DecideShowConnectionPrompt(isOffline: false));
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter ConnectionPromptGateTests`
Expected: FAIL — `DecideShowConnectionPrompt` does not exist.
- [ ] **Step 3: Add the hook, command, gate, and grace timer**
In `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`:
Add a hook property near the other `Show*Modal` hooks (after line 52):
```csharp
// Set by MainWindow to open the worker-connection help dialog.
public Func<Modals.WorkerConnectionModalViewModel, Task>? ShowWorkerConnectionModal { get; set; }
```
Add the gate field + method and the open command (place near `OpenAbout`, around line 271):
```csharp
private bool _connectionPromptShown;
internal bool DecideShowConnectionPrompt(bool isOffline)
{
if (!isOffline) return false;
if (_connectionPromptShown) return false;
_connectionPromptShown = true;
return true;
}
private async Task OpenWorkerConnectionHelpAsync()
{
var vm = new Modals.WorkerConnectionModalViewModel(_workerLocator, _installerLocator);
if (ShowWorkerConnectionModal is not null) await ShowWorkerConnectionModal(vm);
}
[RelayCommand]
private Task OpenWorkerConnectionHelp() => OpenWorkerConnectionHelpAsync();
```
Add the grace timer field near `_clearTimer` (line 74):
```csharp
private readonly System.Timers.Timer _connectTimer = new(12_000) { AutoReset = false };
```
Wire and start it inside the **public** constructor (after the `_primeStatusTimer.Elapsed` wiring, near line 222 — NOT in the parameterless test constructor):
```csharp
_connectTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() =>
{
if (DecideShowConnectionPrompt(IsOffline)) _ = OpenWorkerConnectionHelpAsync();
});
_connectTimer.Start();
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter ConnectionPromptGateTests`
Expected: PASS (2 tests).
- [ ] **Step 5: Build the app**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded.
- [ ] **Step 6: Commit**
```bash
git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs tests/ClaudeDo.Ui.Tests/ConnectionPromptGateTests.cs
git commit -m "feat(ui): prompt once on worker connection failure with grace timer"
```
---
## Task 9: Wire the modal in MainWindow + clickable status pill
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml`
- [ ] **Step 1: Wire the dialog hook**
In `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`, inside `OnDataContextChanged`, after the existing `vm.ShowRepoImportModal = ...` block (line 70), add:
```csharp
vm.ShowWorkerConnectionModal = async (connVm) =>
{
var dlg = new WorkerConnectionModalView { DataContext = connVm };
connVm.CloseAction = () => dlg.Close();
await dlg.ShowDialog(this);
};
```
(`ClaudeDo.Ui.Views.Modals` is already imported at line 10.)
- [ ] **Step 2: Make the status pill a button**
In `src/ClaudeDo.Ui/Views/MainWindow.axaml`, replace the left "connection pill" `StackPanel` (lines 190-202) with a `Button` wrapping the same content:
```xml
<!-- Left: connection pill (click to open worker help) -->
<Button DockPanel.Dock="Left"
Command="{Binding OpenWorkerConnectionHelpCommand}"
Background="Transparent" BorderThickness="0" Padding="0"
Cursor="Hand" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusRunningBrush}"
IsVisible="{Binding Worker.IsConnected}"/>
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusReviewBrush}"
IsVisible="{Binding Worker.IsReconnecting}"/>
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusErrorBrush}"
IsVisible="{Binding IsOffline}"/>
<TextBlock Classes="eyebrow"
Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
```
- [ ] **Step 3: Build the app**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded.
- [ ] **Step 4: Manual verification**
Start the worker (or leave it stopped) and run the App:
- Worker stopped → after ~12s the "WORKER NOT REACHABLE" dialog appears once. **Start Worker** launches it (footer pill turns ONLINE); **Rerun Installer** launches the installer and exits; **Dismiss** closes and does not reappear automatically.
- Click the footer status pill anytime → the dialog reopens.
- Worker running before launch → no dialog appears.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/Views/MainWindow.axaml
git commit -m "feat(ui): wire worker connection modal and make status pill clickable"
```
---
## Task 10: Full build + test sweep
- [ ] **Step 1: Build the touched projects**
Run:
```bash
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj
dotnet build src/ClaudeDo.Installer/ClaudeDo.Installer.csproj
```
Expected: both Build succeeded.
- [ ] **Step 2: Run the affected test suites**
Run:
```bash
dotnet test tests/ClaudeDo.Installer.Tests
dotnet test tests/ClaudeDo.Ui.Tests
```
Expected: all pass; no references to the deleted `ScheduledTaskXml`.
- [ ] **Step 3: Final commit (if any stragglers)**
```bash
git add -A
git commit -m "chore: worker lifecycle redesign cleanup" || echo "nothing to commit"
```

View File

@@ -0,0 +1,96 @@
# UI Normalization & Single Source of Truth — Design
Date: 2026-05-30
Status: Approved
## Goal
Make working on the ClaudeDo UI simpler by establishing the design tokens as the single source of truth for **every** visual value, eliminating duplicated styles, and providing reusable helpers for the patterns that are currently copy-pasted across views. Accept minor visual shifts where current values don't match the token scale — consistency is the priority over pixel-preservation.
## Scope decisions (locked)
- **Lane C (full normalization)** — global defaults + shared helpers + tokenize every hardcoded font/spacing/radius/color.
- **Normalization strategy: B (snap to existing scale).** Stray values round to the nearest existing token; off-palette colors fold into the closest design brush. The token vocabulary stays small; the UI shifts slightly in places and is verified by human eyeball.
- Badge colors collapse to palette (option A): blue is dropped.
## 1. Global defaults — `src/ClaudeDo.App/App.axaml`
Add application-level default styles so unstyled controls inherit the intended look instead of falling back to FluentTheme's Segoe UI:
- Default `FontFamily` = `{DynamicResource SansFont}` (Inter Tight) for text-bearing controls (`TextBlock`, `TextBox`, `Button`, `ComboBox`, `CheckBox`, `NumericUpDown`, `TabItem`).
- Default `FontSize` baseline = `{StaticResource FontSizeBody}` (13) where a control has no more specific style.
- Controls that need mono (`MonoFont`) continue to opt in explicitly via their class/style.
This single change fixes the Settings modal font and every other bare-Segoe-UI label across the app.
## 2. Tokens = source of truth — `src/ClaudeDo.Ui/Design/Tokens.axaml`
### Fonts — snap to the existing scale
Existing tokens: Eyebrow=10, Mono=11, Micro=11, Body=13, TaskTitle=14, H3=18, H2=24, H1=32.
- `9 → 10` (FontSizeEyebrow)
- `12 → 13` (FontSizeBody)
- `16 → 18` (FontSizeH3)
- Every `FontSize="N"` literal across all views/styles becomes a `{StaticResource FontSize*}` reference. No new size tokens are added.
### Spacing / radius — snap to the existing scale
- Modal body padding `16` / `20 → 18` (SpaceXl); the vertical component `12` stays `SpaceMd`.
- Corner radius `4 → 6` (ButtonCornerRadius).
- Text inputs (TextBox) standardize on `InputCornerRadius` (8); the `6` currently on DetailsIslandView TextBoxes moves to 8.
### Colors — fold off-palette into the palette
Add semantic brushes where a recurring role genuinely needs one, but reuse existing palette brushes wherever possible:
- **Connection-status dots** (MainWindow): green `#4CAF50``StatusRunningBrush`; amber `#FFA726``StatusReviewBrush`; red `#EF5350``StatusErrorBrush`. Also applies to the `#EF5350` literals in WorktreesOverviewModal.
- **Planning/draft badges** (IslandStyles `DraftBadgeBrush`/`PlanningBadgeBrush`/`PlannedBadgeBrush`): re-point to palette — draft → `TextMuteBrush`, planning → `PeatBrush`, planned → `SageBrush`. Blue dropped.
- **Named-color literals:** `OrangeRed` / `Orange``BloodBrush`; `White``TextBrush` (or `DeepBrush` where it sits on an accent fill, e.g. primary button text).
- **Terminal background** `#FF080C0B` (terminal + task-live-tail) → `VoidBrush` (`#FF0A0E0C`).
- **Status alpha-tints:** the repeated `#1F<hue>` fills and `#4C<hue>` borders used by chips and agent-strips become named brushes defined once in Tokens (e.g. `RunningTintBrush` / `RunningTintBorderBrush`, and the same for review/error/queued), then referenced from IslandStyles. The `#26<hue>` worktree-badge tints and `#147C9166` agent-strip tints fold into the same named tint family (snap the alpha to one value per family).
- **Island hairline overlay** `#0DFFFFFF` → a named `HairlineOverlayBrush` token.
## 3. Shared helpers
### `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
Promote the styles currently copy-pasted into modals into the shared stylesheet, then delete the per-modal copies:
- `Button.primary` — standardize on **one** definition: `AccentDimBrush` background + `AccentBrush` border + `TextBrush` foreground (matching the existing `Button.btn.primary` variant). Resolves the AccentBrush-vs-AccentDimBrush divergence.
- `Button.danger``BloodBrush` background + `TextBrush` foreground.
- `TextBlock.field-label` — FontSize Micro (11), `TextDimBrush`, bottom margin 4.
- `TextBlock.section-label` already exists in IslandStyles; remove the duplicate local copies.
### New control: `ModalShell` (`src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml`)
A reusable `TemplatedControl` / `UserControl` providing the chrome every modal re-implements:
- Title bar: mono uppercase title (FontSize Mono, LetterSpacing 1.4), draggable region, ✕ close button (`icon-btn`).
- Outer border (SurfaceBrush bg, LineBrush border, ModalCornerRadius).
- Content slot for the body.
- Optional footer slot for action buttons (right-aligned).
- Exposes: `Title` (string), `Body` content, `Footer` content, and a `CloseCommand`.
The 8 modal windows (Settings, ListSettings, Merge, About, UnfinishedPlanning, RepoImport, Diff, PlanningDiff, ConflictResolution) migrate to wrap their content in `ModalShell` instead of re-declaring titlebar/border/footer grids. Window-level concerns (Width/Height, KeyBindings, WindowDecorations) stay on the `Window`; only the inner chrome is replaced.
## 4. Bug fixes (folded into the migration)
- `TaskRowView.axaml` schedule flyout: `BorderBrush="{DynamicResource BorderBrush}"``{DynamicResource LineBrush}` (the `BorderBrush` key does not exist in Tokens; current runtime resource-not-found).
- `DiffModalView.axaml`, `PlanningDiffView.axaml`, `ConflictResolutionView.axaml`: convert all `{StaticResource <token>}` references to `{DynamicResource <token>}` to match the rest of the app and survive theme changes. (Style-internal `Setter` references that must stay `StaticResource` for Avalonia reasons are left as-is; only token lookups in element attributes are converted.)
## 5. Verification
- `dotnet build` per project (`.slnx` requires .NET 9 — build individual csproj):
- `src/ClaudeDo.App/ClaudeDo.App.csproj` (pulls in Ui + Data)
- `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
- A clean build confirms XAML compiles and all resource keys resolve (compiled bindings + StaticResource keys are validated at build time).
- Human visual pass: launch the app and walk each view/modal against a per-view checklist (provided with the plan), since lane B intentionally shifts some values. The eyeball is the regression check.
## Sequencing
1. Tokens.axaml: add new named brushes (tints, status, hairline), re-point badge brushes. (No behavior change yet.)
2. App.axaml: global font/size defaults.
3. IslandStyles.axaml: promote shared styles (primary/danger/field-label), replace internal hardcoded values with token refs.
4. Per-view migration: replace every hardcoded FontSize/spacing/radius/color with token refs; snap stray values.
5. ModalShell control + migrate the 8 modals.
6. Bug fixes (BorderBrush key, Static→Dynamic in the three views).
7. Build all projects; produce visual-check checklist.
## Out of scope
- No layout/structure redesign — only values and shared chrome.
- No new features.
- No changes to ViewModels or behavior (ModalShell migration is markup-only; existing `CancelCommand` etc. bind through unchanged).

View File

@@ -0,0 +1,153 @@
# Worker Lifecycle Redesign
**Date:** 2026-06-01
**Status:** Approved (design)
## Problem
The worker process has multiple competing owners, which collide in development and
muddy production behavior:
- The App auto-spawns its own worker on startup (`EnsureWorkerRunningAsync`,
`IslandsShellViewModel.cs:310`, called at line 224) ~4s after launch if it isn't
yet connected. In the IDE "Start Everything" multilaunch — which already runs the
worker via the `http` launch profile (`dotnet run`) — this produces a *second*
worker that fails to bind to `127.0.0.1:47821` and dies, surfacing a stray console
with a "failed to bind to address" error.
- Production autostart uses a per-user logon **Scheduled Task** (`RegisterAutostartStep`
+ `ScheduledTaskXml`), which the user wants to replace with a simpler Startup-folder
shortcut.
- When the App can't reach the worker, the only feedback is a silent "Offline" pill in
the footer — no guidance to the user.
## Goal
Establish a single owner for the worker lifecycle and make connection failures
actionable:
1. The worker is owned **externally** — a per-user **Startup-folder shortcut** in
production (replacing the Scheduled Task), or the IDE in development.
2. The App **only connects**; it never auto-spawns a worker.
3. When the App can't connect, it shows a one-time prompt offering **Start Worker**,
**Rerun Installer**, or **Dismiss**, plus a clickable Offline pill to reopen it.
## Non-Goals
- No change to the IDE dev setup. The "Start Everything" multilaunch keeps running the
worker via the `http` profile (console with live logs); the duplicate/bind-error
worker disappears purely because the App no longer auto-spawns. Rider run configs live
in `.idea/.../workspace.xml` (per-user, gitignored) and are out of scope.
- No change to the SignalR hub URL, port, reconnect policy, or the worker's
single-instance mutex.
## Design
### Component 1 — Installer: Scheduled Task → Startup-folder shortcut
**`RegisterAutostartStep`** (`src/ClaudeDo.Installer/Steps/RegisterAutostartStep.cs`)
- Replace the task-XML build + `schtasks /Create` with creation of a `.lnk` in the
per-user Startup folder (`Environment.SpecialFolder.Startup`) targeting
`{InstallDirectory}\worker\ClaudeDo.Worker.exe`. The worker is `WinExe`, so it launches
with no console window.
- **Migration:** keep the existing legacy Windows-service removal, and **add** removal of
the old scheduled task: `schtasks.exe /Delete /TN "ClaudeDoWorker" /F` (best-effort),
so existing installs migrate cleanly to the shortcut model.
**`StartWorkerStep`** (`src/ClaudeDo.Installer/Steps/StartWorkerStep.cs`)
- Replace `schtasks /Run /TN ClaudeDoWorker` with a direct
`Process.Start(new ProcessStartInfo(workerExe) { UseShellExecute = true })`.
**`StopWorkerStep`** (`src/ClaudeDo.Installer/Steps/StopWorkerStep.cs`)
- Drop the `schtasks /End` call. Keep the existing install-dir-scoped process kill, which
is the real stop mechanism.
**`UninstallRunner`** (`src/ClaudeDo.Installer/Core/UninstallRunner.cs`)
- Keep the existing `schtasks /Delete` and `sc delete` (migration/legacy cleanup).
- **Add** deletion of the Startup-folder `.lnk` alongside the existing Start Menu /
Desktop shortcut removal.
**Shared shortcut helper**
- Extract the `IShellLink` COM interop currently embedded in `CreateShortcutsStep` into a
shared `src/ClaudeDo.Installer/Core/ShortcutFactory.cs` (`CreateShortcut(path, target,
workingDir, description)`). Both `CreateShortcutsStep` and `RegisterAutostartStep` use it.
**Cleanup**
- Delete `src/ClaudeDo.Installer/Core/ScheduledTaskXml.cs` once unreferenced.
The autostart shortcut name and location: `ClaudeDo Worker.lnk` in
`Environment.SpecialFolder.Startup`, working directory `{InstallDirectory}\worker`.
### Component 2 — App: stop auto-spawning the worker
**`IslandsShellViewModel`** (`src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`)
- Remove the `_ = EnsureWorkerRunningAsync();` call (line 224) and the
`EnsureWorkerRunningAsync` method + its `_ensureRunningAttempted` flag.
- Keep the worker-launch logic (`RestartWorkerService`, which finds the worker exe via
`WorkerLocator` and starts it) — it becomes the backing action for the prompt's
**Start Worker** button. The existing `RestartWorkerAsync` command stays.
### Component 3 — App: connection-failure prompt
**New dialog** `WorkerConnectionModalViewModel`
(`src/ClaudeDo.Ui/ViewModels/Modals/WorkerConnectionModalViewModel.cs`) +
`WorkerConnectionModalView` (`src/ClaudeDo.Ui/Views/Modals/`).
- Buttons: **Start Worker**, **Rerun Installer**, **Dismiss**.
- Uses the established dialog pattern: a `Func<WorkerConnectionModalViewModel, Task>`
hook on `IslandsShellViewModel` set by `MainWindow` (mirroring `ShowAboutModal`), and
the dialog resolves a `TaskCompletionSource` on button press.
- **Start Worker** → `WorkerLocator.Find()` + `Process.Start` (reuse the
`RestartWorkerService` path). **Rerun Installer**`InstallerLocator.Find()` + launch
+ `Environment.Exit(0)` (same pattern as the existing `UpdateNow` command).
**Dismiss** → close.
**Trigger logic** (in `IslandsShellViewModel`)
- A one-shot grace timer (~12s) started on construction/startup. When it elapses, if the
worker is still offline (`IsOffline` — not connected and not reconnecting) and the
prompt hasn't been shown yet (`_connectionPromptShown`), show the dialog once and set
the flag.
- If the worker connects before the grace elapses, the prompt is never shown.
**Clickable Offline pill** (`src/ClaudeDo.Ui/Views/MainWindow.axaml`)
- Turn the footer status pill into a button bound to a command that opens the same dialog
on demand (independent of the one-shot flag), so the user can reopen guidance anytime
while offline.
### Component 4 — Dev
No code change (see Non-Goals).
## Data Flow
```
Startup (production):
Windows logon -> Startup-folder .lnk -> ClaudeDo.Worker.exe (WinExe, mutex-guarded)
App launches -> WorkerClient connects to 127.0.0.1:47821
connected within grace -> Online pill, no prompt
still offline after ~12s -> WorkerConnectionModal (once)
User clicks Offline pill (anytime offline) -> WorkerConnectionModal
Start Worker -> Process.Start(worker exe)
Rerun Installer -> Process.Start(installer), Environment.Exit(0)
Dismiss -> close
```
## Error Handling
- Worker exe / installer not found (`Locator.Find()` returns null): the corresponding
dialog button is a no-op (consistent with existing `UpdateNow` behavior); the dialog
stays open so the user can pick another action.
- Startup-shortcut creation failure in the installer: surfaced as a failed install step
(`StepResult.Fail`), same as the current task-registration failure path.
- Legacy scheduled-task deletion is best-effort and never fails the install.
## Testing
- **`Installer.Tests`**: `RegisterAutostartStep` creates the Startup `.lnk` at the
expected path with the correct target, and issues the legacy-task delete command.
`UninstallRunner` removes the Startup `.lnk`.
- **`Ui.Tests`**: prompt trigger logic — grace elapsed while offline shows the prompt
exactly once; a connection established before grace suppresses it; the clickable-pill
command opens the dialog regardless of the one-shot flag. (Abstract the dialog-show
hook so it can be asserted without real UI.)
- **Manual**: dialog buttons (Start Worker / Rerun Installer / Dismiss) and the clickable
Offline pill in a running App.

View File

@@ -9,6 +9,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ClaudeDo.Ui/Design/Tokens.axaml" />
<ResourceInclude Source="avares://ClaudeDo.Ui/Views/Controls/ModalShell.axaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Converters -->
@@ -31,6 +32,13 @@
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />
<!-- Global defaults: every Window inherits Inter Tight + body size.
Controls that need mono opt in via their own class/style. -->
<Style Selector="Window">
<Setter Property="FontFamily" Value="{DynamicResource SansFont}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeBody}" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentGlowBrush}"/>
</Style>

View File

@@ -0,0 +1,482 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260416064948_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,498 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260420075929_AddTaskFlagsAndNotes")]
partial class AddTaskFlagsAndNotes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,572 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260421113614_AddAppSettings")]
partial class AddAppSettings
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 30,
DefaultModel = "sonnet",
DefaultPermissionMode = "bypassPermissions",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,581 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260422120000_AddTaskSortOrder")]
partial class AddTaskSortOrder
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 30,
DefaultModel = "sonnet",
DefaultPermissionMode = "bypassPermissions",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,609 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260423154708_AddPlanningSupport")]
partial class AddPlanningSupport
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 30,
DefaultModel = "sonnet",
DefaultPermissionMode = "bypassPermissions",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,613 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260424212250_AddTaskCreatedBy")]
partial class AddTaskCreatedBy
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 100,
DefaultModel = "sonnet",
DefaultPermissionMode = "bypassPermissions",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,632 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260427082248_AddPlanningPhaseAndBlockedBy")]
partial class AddPlanningPhaseAndBlockedBy
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 100,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,632 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260427130058_RetireLegacyTaskStatus")]
partial class RetireLegacyTaskStatus
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 100,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tags", (string)null);
b.HasData(
new
{
Id = 1L,
Name = "agent"
},
new
{
Id = 2L,
Name = "manual"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("list_tags", b =>
{
b.Property<string>("list_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("list_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("list_tags", (string)null);
});
modelBuilder.Entity("task_tags", b =>
{
b.Property<string>("task_id")
.HasColumnType("TEXT");
b.Property<long>("tag_id")
.HasColumnType("INTEGER");
b.HasKey("task_id", "tag_id");
b.HasIndex("tag_id");
b.ToTable("task_tags", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("list_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", null)
.WithMany()
.HasForeignKey("list_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("task_tags", b =>
{
b.HasOne("ClaudeDo.Data.Models.TagEntity", null)
.WithMany()
.HasForeignKey("tag_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("task_id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,587 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260519044715_RemoveTags")]
partial class RemoveTags
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 100,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("enabled");
b.Property<DateOnly>("EndDate")
.HasColumnType("TEXT")
.HasColumnName("end_date");
b.Property<DateTimeOffset?>("LastRunAt")
.HasColumnType("TEXT")
.HasColumnName("last_run_at");
b.Property<string>("PromptOverride")
.HasColumnType("TEXT")
.HasColumnName("prompt_override");
b.Property<DateOnly>("StartDate")
.HasColumnType("TEXT")
.HasColumnName("start_date");
b.Property<TimeSpan>("TimeOfDay")
.HasColumnType("TEXT")
.HasColumnName("time_of_day");
b.Property<bool>("WorkdaysOnly")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("workdays_only");
b.HasKey("Id");
b.ToTable("prime_schedules", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,591 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260529142614_AddRepoImportFolders")]
partial class AddRepoImportFolders
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 100,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("enabled");
b.Property<DateOnly>("EndDate")
.HasColumnType("TEXT")
.HasColumnName("end_date");
b.Property<DateTimeOffset?>("LastRunAt")
.HasColumnType("TEXT")
.HasColumnName("last_run_at");
b.Property<string>("PromptOverride")
.HasColumnType("TEXT")
.HasColumnName("prompt_override");
b.Property<DateOnly>("StartDate")
.HasColumnType("TEXT")
.HasColumnName("start_date");
b.Property<TimeSpan>("TimeOfDay")
.HasColumnType("TEXT")
.HasColumnName("time_of_day");
b.Property<bool>("WorkdaysOnly")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("workdays_only");
b.HasKey("Id");
b.ToTable("prime_schedules", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,26 @@
using System.IO;
namespace ClaudeDo.Installer.Core;
public static class AutostartShortcut
{
public const string FileName = "ClaudeDo Worker.lnk";
public static string DefaultStartupDir =>
Environment.GetFolderPath(Environment.SpecialFolder.Startup);
public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName);
public static void Install(string startupDir, string workerExe)
{
Directory.CreateDirectory(startupDir);
var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir;
ShortcutFactory.CreateShortcut(PathIn(startupDir), workerExe, workingDir, "ClaudeDo background worker");
}
public static void Remove(string startupDir)
{
var path = PathIn(startupDir);
if (File.Exists(path)) File.Delete(path);
}
}

View File

@@ -1,52 +0,0 @@
using System.Security;
namespace ClaudeDo.Installer.Core;
public static class ScheduledTaskXml
{
public static string Build(string userId, string workerExePath, int restartIntervalMinutes)
{
var minutes = restartIntervalMinutes < 1 ? 1 : restartIntervalMinutes;
var user = SecurityElement.Escape(userId);
var cmd = SecurityElement.Escape(workerExePath);
return $"""
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>ClaudeDo background worker (per-user).</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>{user}</UserId>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>{user}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<Hidden>true</Hidden>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<RestartOnFailure>
<Interval>PT{minutes}M</Interval>
<Count>3</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<Exec>
<Command>{cmd}</Command>
</Exec>
</Actions>
</Task>
""";
}
}

View File

@@ -0,0 +1,49 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace ClaudeDo.Installer.Core;
public static class ShortcutFactory
{
public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
{
var link = (IShellLink)new ShellLink();
link.SetPath(targetPath);
link.SetWorkingDirectory(workingDir);
link.SetDescription(description);
link.SetIconLocation(targetPath, 0);
var file = (IPersistFile)link;
file.Save(shortcutPath, false);
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink { }
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLink
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
}

View File

@@ -34,9 +34,10 @@ public sealed class UninstallRunner
$"Cannot uninstall: worker did not stop cleanly. {stopResult.ErrorMessage} " +
"Kill the worker manually and re-run uninstall.");
// 3) Unregister the autostart task, and best-effort remove any legacy service.
progress.Report("Removing autostart task...");
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{StopWorkerStep.TaskName}\" /F", null, progress, ct);
// 3) Best-effort removal of the legacy scheduled task and Windows service
// (older installs; current installs autostart via a Startup-folder shortcut).
progress.Report("Removing legacy autostart task...");
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{StopWorkerStep.LegacyTaskName}\" /F", null, progress, ct);
await ProcessRunner.RunAsync("sc.exe", "delete ClaudeDoWorker", null, progress, ct);
// 3b) Remove Apps & Features registry entry (best-effort).
@@ -58,6 +59,7 @@ public sealed class UninstallRunner
TryDeleteFile(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs", "ClaudeDo.lnk"));
TryDeleteFile(AutostartShortcut.PathIn(AutostartShortcut.DefaultStartupDir));
// 5) Delete install directory. Track success so we can report partial state.
var failures = new List<string>();

View File

@@ -1,7 +1,4 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
@@ -23,7 +20,7 @@ public sealed class CreateShortcutsStep : IInstallStep
"Programs");
Directory.CreateDirectory(startMenuDir);
var startMenuPath = Path.Combine(startMenuDir, "ClaudeDo.lnk");
CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager");
ShortcutFactory.CreateShortcut(startMenuPath, appExe, workingDir, "ClaudeDo Task Manager");
progress.Report($"Created Start Menu shortcut: {startMenuPath}");
// Desktop shortcut (optional)
@@ -32,7 +29,7 @@ public sealed class CreateShortcutsStep : IInstallStep
var desktopPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
"ClaudeDo.lnk");
CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager");
ShortcutFactory.CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager");
progress.Report($"Created Desktop shortcut: {desktopPath}");
}
@@ -44,48 +41,5 @@ public sealed class CreateShortcutsStep : IInstallStep
}
}
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
{
var link = (IShellLink)new ShellLink();
link.SetPath(targetPath);
link.SetWorkingDirectory(workingDir);
link.SetDescription(description);
link.SetIconLocation(targetPath, 0);
var file = (IPersistFile)link;
file.Save(shortcutPath, false);
}
#region COM Interop for IShellLink
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private class ShellLink { }
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLink
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
#endregion
}

View File

@@ -1,12 +1,11 @@
using System.IO;
using System.Security.Principal;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class RegisterAutostartStep : IInstallStep
{
public const string TaskName = "ClaudeDoWorker";
public const string LegacyTaskName = "ClaudeDoWorker";
private const string LegacyServiceName = "ClaudeDoWorker";
public string Name => "Register Autostart";
@@ -34,24 +33,19 @@ public sealed class RegisterAutostartStep : IInstallStep
}
}
// 2) Register (or replace) the per-user logon task.
var userId = WindowsIdentity.GetCurrent().Name;
var minutes = Math.Max(1, ctx.RestartDelayMs / 60000);
var xml = ScheduledTaskXml.Build(userId, workerExe, minutes);
// 2) Migrate away the legacy logon scheduled task if present (best-effort).
progress.Report("Removing legacy logon task...");
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
var xmlPath = Path.Combine(Path.GetTempPath(), $"ClaudeDoWorker-{Guid.NewGuid():N}.xml");
await File.WriteAllTextAsync(xmlPath, xml, new System.Text.UnicodeEncoding(false, true), ct);
// 3) Register per-user autostart via a Startup-folder shortcut.
progress.Report("Creating Startup shortcut...");
try
{
progress.Report("Registering logon task...");
var (exit, output) = await ProcessRunner.RunAsync(
"schtasks.exe", $"/Create /TN \"{TaskName}\" /XML \"{xmlPath}\" /F", null, progress, ct);
if (exit != 0)
return StepResult.Fail($"schtasks /Create failed (exit {exit}): {output}");
AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
}
finally
catch (Exception ex)
{
try { File.Delete(xmlPath); } catch { /* best effort */ }
return StepResult.Fail($"Failed to create Startup shortcut: {ex.Message}");
}
return StepResult.Ok();

View File

@@ -1,19 +1,28 @@
using System.Diagnostics;
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class StartWorkerStep : IInstallStep
{
public const string TaskName = "ClaudeDoWorker";
public string Name => "Start Worker";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
public Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe");
if (!File.Exists(workerExe))
return Task.FromResult(StepResult.Fail($"Worker executable not found: {workerExe}"));
progress.Report("Starting worker...");
var (exit, output) = await ProcessRunner.RunAsync("schtasks.exe", $"/Run /TN \"{TaskName}\"", null, progress, ct);
if (exit != 0)
return StepResult.Fail($"schtasks /Run failed (exit {exit}): {output}");
return StepResult.Ok();
try
{
Process.Start(new ProcessStartInfo(workerExe) { UseShellExecute = true });
return Task.FromResult(StepResult.Ok());
}
catch (Exception ex)
{
return Task.FromResult(StepResult.Fail($"Failed to start worker: {ex.Message}"));
}
}
}

View File

@@ -6,16 +6,13 @@ namespace ClaudeDo.Installer.Steps;
public sealed class StopWorkerStep : IInstallStep
{
public const string TaskName = "ClaudeDoWorker";
public const string LegacyTaskName = "ClaudeDoWorker";
public const string ProcessName = "ClaudeDo.Worker";
public string Name => "Stop Worker";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
progress.Report("Stopping worker task (if running)...");
await ProcessRunner.RunAsync("schtasks.exe", $"/End /TN \"{TaskName}\"", null, progress, ct);
progress.Report("Stopping worker process (if running)...");
var installDir = ctx.InstallDirectory;
foreach (var p in Process.GetProcessesByName(ProcessName))

View File

@@ -85,9 +85,9 @@
<StreamGeometry x:Key="Icon.Settings">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z</StreamGeometry>
<!-- Badge brushes -->
<SolidColorBrush x:Key="DraftBadgeBrush" Color="#4A5568"/>
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="#D69E2E"/>
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="#3182CE"/>
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
</Styles.Resources>
@@ -120,7 +120,7 @@
<!-- ============================================================ -->
<Style Selector="Border.island">
<Setter Property="Background" Value="{StaticResource IslandBackgroundBrush}" />
<Setter Property="BorderBrush" Value="#0DFFFFFF" />
<Setter Property="BorderBrush" Value="{StaticResource HairlineOverlayBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource IslandCornerRadius}" />
<Setter Property="BoxShadow" Value="{StaticResource IslandShadow}" />
@@ -146,30 +146,30 @@
</Style>
<Style Selector="Border.chip > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<!-- Status variants — tint background 12% alpha of the status hue -->
<Style Selector="Border.chip.running">
<Setter Property="Background" Value="#1F7C9166" />
<Setter Property="BorderBrush" Value="#4C7C9166" />
<Setter Property="Background" Value="{StaticResource RunningTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource RunningTintBorderBrush}" />
</Style>
<Style Selector="Border.chip.running > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusRunningBrush}" />
</Style>
<Style Selector="Border.chip.review">
<Setter Property="Background" Value="#1FD4A574" />
<Setter Property="BorderBrush" Value="#4CD4A574" />
<Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
</Style>
<Style Selector="Border.chip.review > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" />
</Style>
<Style Selector="Border.chip.error">
<Setter Property="Background" Value="#1FC87060" />
<Setter Property="BorderBrush" Value="#4CC87060" />
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
</Style>
<Style Selector="Border.chip.error > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusErrorBrush}" />
@@ -177,8 +177,8 @@
<!-- queued → Sage (#8B9D7A) -->
<Style Selector="Border.chip.queued">
<Setter Property="Background" Value="#1F8B9D7A" />
<Setter Property="BorderBrush" Value="#4C8B9D7A" />
<Setter Property="Background" Value="{StaticResource QueuedTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource QueuedTintBorderBrush}" />
</Style>
<Style Selector="Border.chip.queued > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusQueuedBrush}" />
@@ -203,7 +203,7 @@
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="Transitions">
<Transitions>
@@ -216,11 +216,6 @@
<Setter Property="Background" Value="{StaticResource Surface3Brush}" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
</Style>
<Style Selector="Button.btn.primary">
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Icon button: 24×24 square with hover surface -->
<Style Selector="Button.icon-btn">
@@ -248,7 +243,7 @@
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource InputCornerRadius}" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
</Style>
@@ -310,8 +305,9 @@
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.10"/>
<BrushTransition Property="BorderBrush" Duration="0:0:0.10"/>
<BrushTransition Property="Background" Duration="0:0:0.12" />
<BrushTransition Property="BorderBrush" Duration="0:0:0.12" />
<ThicknessTransition Property="Margin" Duration="0:0:0.15" />
</Transitions>
</Setter>
</Style>
@@ -358,22 +354,22 @@
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
</Style>
<Style Selector="Border.agent-strip.running">
<Setter Property="Background" Value="#147C9166" />
<Setter Property="BorderBrush" Value="#4C7C9166" />
<Setter Property="Background" Value="{StaticResource RunningTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource RunningTintBorderBrush}" />
</Style>
<Style Selector="Border.agent-strip.review">
<Setter Property="Background" Value="#14D4A574" />
<Setter Property="BorderBrush" Value="#4CD4A574" />
<Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
</Style>
<Style Selector="Border.agent-strip.error">
<Setter Property="Background" Value="#14C87060" />
<Setter Property="BorderBrush" Value="#4CC87060" />
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
</Style>
<!-- queued → Sage tint -->
<Style Selector="Border.agent-strip.queued">
<Setter Property="Background" Value="#148B9D7A" />
<Setter Property="BorderBrush" Value="#4C8B9D7A" />
<Setter Property="Background" Value="{StaticResource QueuedTintBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource QueuedTintBorderBrush}" />
</Style>
<!-- idle → neutral (same as base, explicit for clarity) -->
@@ -386,7 +382,7 @@
<!-- TERMINAL / LOG -->
<!-- ============================================================ -->
<Style Selector="Border.terminal">
<Setter Property="Background" Value="#FF080C0B" />
<Setter Property="Background" Value="{StaticResource VoidBrush}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="12" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
@@ -394,7 +390,7 @@
</Style>
<Style Selector="Border.terminal TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<Style Selector="Border.terminal TextBlock[Tag=log-sys]">
@@ -449,7 +445,7 @@
</Style>
<Style Selector="Border.live-chip > StackPanel > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="9" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
<Setter Property="LetterSpacing" Value="1.2" />
</Style>
@@ -471,7 +467,7 @@
<!-- Terminal log-line timestamp column -->
<Style Selector="TextBlock.log-ts">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="Width" Value="60" />
<Setter Property="VerticalAlignment" Value="Top" />
@@ -480,7 +476,7 @@
<!-- Kind marker column -->
<Style Selector="TextBlock.log-kind">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="Width" Value="46" />
<Setter Property="VerticalAlignment" Value="Top" />
@@ -554,7 +550,7 @@
<!-- Count badge — larger, high contrast, brighter when the row is active -->
<Style Selector="TextBlock.list-count">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
@@ -565,17 +561,6 @@
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- ============================================================ -->
<!-- LIST SECTION HEADER -->
<!-- ============================================================ -->
<Style Selector="TextBlock.list-section-label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="9" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="Margin" Value="10,10,10,4" />
<Setter Property="LetterSpacing" Value="1.2" />
</Style>
<!-- ============================================================ -->
<!-- SEARCH BOX WRAPPER -->
<!-- ============================================================ -->
@@ -594,7 +579,7 @@
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="4,7" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
</Style>
@@ -618,7 +603,7 @@
</Style>
<Style Selector="Border.kbd > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
@@ -633,7 +618,7 @@
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Cursor" Value="Hand" />
@@ -694,7 +679,7 @@
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontSize" Value="{StaticResource FontSizeTaskTitle}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="MinHeight" Value="20" />
@@ -722,22 +707,6 @@
<!-- TASK ROW — extensions (C2) -->
<!-- ============================================================ -->
<!-- Augment base task-row transitions to include Margin -->
<Style Selector="Border.task-row">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.12" />
<ThicknessTransition Property="Margin" Duration="0:0:0.15" />
</Transitions>
</Setter>
</Style>
<!-- Selected state: rely on the left accent bar from TaskRowView;
no heavy bg or perimeter border. -->
<Style Selector="Border.task-row.selected">
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
</Style>
<!-- Left accent bar for selected row -->
<Style Selector="Border.task-row-accent">
<Setter Property="Width" Value="2" />
@@ -767,7 +736,7 @@
</Style>
<Style Selector="Border.task-row Border.chip > StackPanel > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
@@ -779,12 +748,12 @@
<!-- Diff chip add/del coloring -->
<Style Selector="TextBlock.diff-add">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}" />
</Style>
<Style Selector="TextBlock.diff-del">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource BloodBrush}" />
</Style>
@@ -807,7 +776,7 @@
<!-- LIVE-TAIL PREVIEW ROW -->
<!-- ============================================================ -->
<Style Selector="Border.task-live-tail">
<Setter Property="Background" Value="#FF080C0B" />
<Setter Property="Background" Value="{StaticResource VoidBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
@@ -816,7 +785,7 @@
</Style>
<Style Selector="Border.task-live-tail TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
@@ -863,8 +832,8 @@
<!-- ============================================================ -->
<Style Selector="TextBlock.section-label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="LetterSpacing" Value="1.4" />
</Style>
<Style Selector="TextBlock.section-label.overdue">
@@ -881,9 +850,9 @@
</Style>
<Style Selector="Border.badge > TextBlock">
<Setter Property="FontSize" Value="9"/>
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
</Style>
<Style Selector="Border.badge.draft">
@@ -898,4 +867,153 @@
<Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
</Style>
<!-- ============================================================ -->
<!-- SHARED MODAL STYLES (promoted from per-modal Window.Styles) -->
<!-- ============================================================ -->
<Style Selector="TextBlock.field-label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style Selector="TextBlock.path-mono">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<!-- Self-contained action buttons (same geometry as Button.btn, distinct color).
Used standalone, e.g. Classes="primary" / "danger". -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource BloodBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BloodBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- ============================================================ -->
<!-- CANONICAL TYPOGRAPHY -->
<!-- One class per text role. Small text = 11 (eyebrow/meta/ -->
<!-- field-label/path-mono). Body = 13. Heading = 18. Display = 24.-->
<!-- ============================================================ -->
<!-- Small secondary mono text: timestamps, ids, hints, status, diffstat, age -->
<Style Selector="TextBlock.meta">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<!-- Default body / list values / descriptions -->
<Style Selector="TextBlock.body">
<Setter Property="FontFamily" Value="{StaticResource SansFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<!-- Item / task / detail titles -->
<Style Selector="TextBlock.title">
<Setter Property="FontFamily" Value="{StaticResource SansFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Panel / section headings ("Lists", modal section titles) -->
<Style Selector="TextBlock.heading">
<Setter Property="FontFamily" Value="{StaticResource SansFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeH3}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Main board / island display title -->
<Style Selector="TextBlock.display">
<Setter Property="FontFamily" Value="{StaticResource SansFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeH2}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<!-- ============================================================ -->
<!-- SHARED CONTAINERS (promoted from per-view inline/local styles)-->
<!-- ============================================================ -->
<!-- Bordered card / settings section -->
<Style Selector="Border.section">
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="14" />
<Setter Property="Background" Value="{StaticResource DeepBrush}" />
</Style>
<!-- Stacked content section with a bottom hairline (Details island) -->
<Style Selector="Border.section-divider">
<Setter Property="Padding" Value="18,12" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<!-- Inline danger confirm box -->
<Style Selector="Border.danger-box">
<Setter Property="BorderBrush" Value="{StaticResource BloodBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="12,10" />
</Style>
<!-- Left sidebar pane with vertical hairline (diff/planning views) -->
<Style Selector="Border.sidebar-pane">
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
<Setter Property="BorderThickness" Value="0,0,1,0" />
<Setter Property="Background" Value="{StaticResource DeepBrush}" />
</Style>
<!-- Diff line-number gutter column -->
<Style Selector="TextBlock.diff-lineno">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
</Style>
<!-- Terminal selectable log text (SelectableTextBlock doesn't inherit the TextBlock terminal style) -->
<Style Selector="Border.terminal SelectableTextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<!-- Accent call-to-action button (Send to queue / Continue / Schedule) -->
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
</Styles>

View File

@@ -84,6 +84,19 @@
<SolidColorBrush x:Key="StatusQueuedBrush" Color="{StaticResource StatusQueuedColor}" />
<SolidColorBrush x:Key="StatusIdleBrush" Color="{StaticResource StatusIdleColor}" />
<!-- Subtle white overlay (island hairline border) -->
<SolidColorBrush x:Key="HairlineOverlayBrush" Color="#0DFFFFFF" />
<!-- Status tints (12% fill / 30% border of the status hue) — reused by chips & agent strips -->
<SolidColorBrush x:Key="RunningTintBrush" Color="#1F7C9166" />
<SolidColorBrush x:Key="RunningTintBorderBrush" Color="#4C7C9166" />
<SolidColorBrush x:Key="ReviewTintBrush" Color="#1FD4A574" />
<SolidColorBrush x:Key="ReviewTintBorderBrush" Color="#4CD4A574" />
<SolidColorBrush x:Key="ErrorTintBrush" Color="#1FC87060" />
<SolidColorBrush x:Key="ErrorTintBorderBrush" Color="#4CC87060" />
<SolidColorBrush x:Key="QueuedTintBrush" Color="#1F8B9D7A" />
<SolidColorBrush x:Key="QueuedTintBorderBrush" Color="#4C8B9D7A" />
<!-- Window-body gradient layers (apply as LinearGradientBrush in the main content Border) -->
<LinearGradientBrush x:Key="DesktopBackgroundBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#FF05070A" />
@@ -149,11 +162,11 @@
<FontFamily x:Key="MonoFamily">avares://ClaudeDo.Ui/Assets/Fonts/#JetBrains Mono, IBM Plex Mono, Cascadia Mono, Consolas, monospace</FontFamily>
<!-- Type scale -->
<x:Double x:Key="FontSizeEyebrow">10</x:Double> <!-- uppercase label, 0.14em tracking -->
<x:Double x:Key="FontSizeEyebrow">11</x:Double> <!-- uppercase label, 0.14em tracking -->
<x:Double x:Key="FontSizeMono">11</x:Double> <!-- chips, log lines, filepaths -->
<x:Double x:Key="FontSizeMicro">11</x:Double> <!-- meta rows -->
<x:Double x:Key="FontSizeBody">13</x:Double>
<x:Double x:Key="FontSizeTaskTitle">14</x:Double>
<x:Double x:Key="FontSizeTaskTitle">13</x:Double>
<x:Double x:Key="FontSizeH3">18</x:Double>
<x:Double x:Key="FontSizeH2">24</x:Double> <!-- island titles ("My Day") -->
<x:Double x:Key="FontSizeH1">32</x:Double>
@@ -162,7 +175,7 @@
<Style x:Key="EyebrowText" Selector="TextBlock.eyebrow">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="LetterSpacing" Value="1.4" />
</Style>

View File

@@ -51,6 +51,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
// Set by MainWindow to open the global worktrees overview dialog.
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; }
// Set by MainWindow to open the worker-connection help dialog.
public Func<WorkerConnectionModalViewModel, Task>? ShowWorkerConnectionModal { get; set; }
[ObservableProperty] private bool _isUpdateBannerVisible;
[ObservableProperty] private string? _updateBannerLatestVersion;
[ObservableProperty] private string? _inlineUpdateStatus;
@@ -72,6 +75,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
public bool ShowLists => WindowWidth >= 780;
private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false };
private readonly System.Timers.Timer _connectTimer = new(12_000) { AutoReset = false };
[ObservableProperty] private string? _primeStatus;
private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false };
@@ -220,8 +224,12 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
};
_primeStatusTimer.Elapsed += (_, _) =>
Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null);
_connectTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() =>
{
if (DecideShowConnectionPrompt(IsOffline)) _ = OpenWorkerConnectionHelpAsync();
});
_connectTimer.Start();
_ = Lists.LoadAsync();
_ = EnsureWorkerRunningAsync();
_updateCheck.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(UpdateCheckService.LastCheckStatus))
@@ -270,6 +278,25 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
if (ShowAboutModal is not null) await ShowAboutModal(vm);
}
private bool _connectionPromptShown;
internal bool DecideShowConnectionPrompt(bool isOffline)
{
if (!isOffline) return false;
if (_connectionPromptShown) return false;
_connectionPromptShown = true;
return true;
}
private async Task OpenWorkerConnectionHelpAsync()
{
var vm = new WorkerConnectionModalViewModel(_workerLocator, _installerLocator);
if (ShowWorkerConnectionModal is not null) await ShowWorkerConnectionModal(vm);
}
[RelayCommand]
private Task OpenWorkerConnectionHelp() => OpenWorkerConnectionHelpAsync();
[RelayCommand]
private async Task OpenRepoImport()
{
@@ -305,20 +332,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
[ObservableProperty] private string? _restartWorkerStatus;
private bool _ensureRunningAttempted;
private async Task EnsureWorkerRunningAsync()
{
if (_ensureRunningAttempted) return;
_ensureRunningAttempted = true;
await Task.Delay(TimeSpan.FromSeconds(4));
if (Worker?.IsConnected == true) return;
var exe = _workerLocator.Find();
if (exe is null) return;
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(exe) { UseShellExecute = true }); }
catch { /* logon task is the primary mechanism; this is a convenience */ }
}
[RelayCommand]
private async Task RestartWorkerAsync()
{

View File

@@ -0,0 +1,45 @@
using System;
using System.Diagnostics;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals;
public sealed partial class WorkerConnectionModalViewModel : ViewModelBase
{
private readonly WorkerLocator _workerLocator;
private readonly InstallerLocator _installerLocator;
public WorkerConnectionModalViewModel(WorkerLocator workerLocator, InstallerLocator installerLocator)
{
_workerLocator = workerLocator;
_installerLocator = installerLocator;
}
public Action? CloseAction { get; set; }
[RelayCommand] private void Close() => CloseAction?.Invoke();
[RelayCommand]
private void StartWorker()
{
var exe = _workerLocator.Find();
if (exe is null) return;
try { Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); }
catch { /* nothing useful to show */ }
CloseAction?.Invoke();
}
[RelayCommand]
private void RerunInstaller()
{
var path = _installerLocator.Find();
if (path is null) return;
try
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
Environment.Exit(0);
}
catch { /* nothing useful to show */ }
}
}

View File

@@ -0,0 +1,43 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls">
<ControlTheme x:Key="{x:Type ctl:ModalShell}" TargetType="ctl:ModalShell">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ModalCornerRadius}"
ClipToBounds="True">
<DockPanel>
<Border Name="PART_TitleBar" DockPanel.Dock="Top" Height="36"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="{TemplateBinding Title}"
FontFamily="{DynamicResource MonoFont}"
FontSize="{DynamicResource FontSizeMono}"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
FontSize="{DynamicResource FontSizeBody}"
Command="{TemplateBinding CloseCommand}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<Border Name="PART_Footer" DockPanel.Dock="Bottom"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
IsVisible="{TemplateBinding Footer, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentPresenter Content="{TemplateBinding Footer}" Margin="16,8"/>
</Border>
<ContentPresenter Content="{TemplateBinding Content}"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -0,0 +1,38 @@
using System;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace ClaudeDo.Ui.Views.Controls;
/// <summary>Reusable modal chrome: titlebar (drag + close) wrapping a body and optional footer.</summary>
public class ModalShell : ContentControl
{
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<ModalShell, string?>(nameof(Title));
public static readonly StyledProperty<object?> FooterProperty =
AvaloniaProperty.Register<ModalShell, object?>(nameof(Footer));
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
AvaloniaProperty.Register<ModalShell, ICommand?>(nameof(CloseCommand));
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<Border>("PART_TitleBar") is { } bar)
bar.PointerPressed += OnTitleBarPressed;
}
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && VisualRoot is Window w)
w.BeginMoveDrag(e);
}
}

View File

@@ -31,7 +31,7 @@
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="CornerRadius" Value="999"/>
<Setter Property="Padding" Value="10,3"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}"/>
<Setter Property="MinHeight" Value="22"/>
</Style>
<Style Selector="Button.quick:pointerover /template/ ContentPresenter">
@@ -61,7 +61,7 @@
<Setter Property="Width" Value="32"/>
<Setter Property="Height" Value="32"/>
<Setter Property="CornerRadius" Value="999"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
@@ -77,7 +77,7 @@
</Style>
<Style Selector="Button.day.selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="TextElement.Foreground" Value="White"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextBrush}"/>
</Style>
<Style Selector="Button.day.selected:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentDimBrush}"/>
@@ -86,7 +86,7 @@
<Style Selector="TextBlock.weekday">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{DynamicResource TextMuteBrush}"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</UserControl.Styles>
@@ -133,12 +133,9 @@
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,2,0,0">
<Button Grid.Column="0" Click="OnPrevMonthClick" Classes="nav" Content="◀"/>
<TextBlock Grid.Column="1" x:Name="MonthHeader"
<TextBlock Grid.Column="1" x:Name="MonthHeader" Classes="title"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
FontSize="13"
Foreground="{DynamicResource TextBrush}"/>
VerticalAlignment="Center"/>
<Button Grid.Column="2" Click="OnNextMonthClick" Classes="nav" Content="▶"/>
</Grid>

View File

@@ -17,17 +17,14 @@
Classes.status-pulse="{Binding IsRunning}"
Margin="0,0,6,0"/>
<TextBlock Grid.Column="1"
Classes="meta"
Text="{Binding AgentStatusLabel}"
FontFamily="{DynamicResource MonoFont}"
FontSize="10"
LetterSpacing="1.2"
Foreground="{DynamicResource TextDimBrush}"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<TextBlock Grid.Column="2"
Classes="meta"
Text="{Binding Model}"
FontFamily="{DynamicResource MonoFont}"
FontSize="10"
Foreground="{DynamicResource TextFaintBrush}"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
@@ -66,16 +63,15 @@
<Grid ColumnDefinitions="Auto,*,Auto"
IsVisible="{Binding WorktreePath, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Grid.Column="0"
Classes="eyebrow"
Text="WORKTREE"
FontFamily="{DynamicResource MonoFont}" FontSize="9"
LetterSpacing="1.2"
Foreground="{DynamicResource TextFaintBrush}"
LetterSpacing="1.2"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<TextBlock Grid.Column="1"
Classes="meta"
Text="{Binding WorktreePath}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextDimBrush}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
<Button Grid.Column="2"
@@ -93,15 +89,14 @@
<PathIcon Data="{StaticResource Icon.GitBranch}" Width="11" Height="11"
Foreground="{DynamicResource AccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding BranchLine}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextDimBrush}"
<TextBlock Classes="meta"
Text="{Binding BranchLine}"
VerticalAlignment="Center"/>
<Border Classes="chip"
IsVisible="{Binding CommitsOnBranch}"
Padding="5,1" CornerRadius="4">
<TextBlock Text="{Binding CommitsOnBranch, StringFormat='{}{0}c'}"
FontFamily="{DynamicResource MonoFont}" FontSize="9"
Padding="5,1" CornerRadius="6">
<TextBlock Classes="meta"
Text="{Binding CommitsOnBranch, StringFormat='{}{0}c'}"
Foreground="{DynamicResource TextFaintBrush}"/>
</Border>
</StackPanel>
@@ -109,10 +104,10 @@
<!-- Row 4: DIFF label + +add del + meter bar -->
<Grid ColumnDefinitions="Auto,Auto,Auto,*">
<TextBlock Grid.Column="0"
Classes="eyebrow"
Text="DIFF"
FontFamily="{DynamicResource MonoFont}" FontSize="9"
LetterSpacing="1.2"
Foreground="{DynamicResource TextFaintBrush}"
LetterSpacing="1.2"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<TextBlock Grid.Column="1"

View File

@@ -21,9 +21,8 @@
Foreground="{DynamicResource BloodBrush}"/>
</Button>
<TextBlock Grid.Column="1"
Classes="meta"
Text="{Binding Task.CreatedAtFormatted}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Button Grid.Column="2" Classes="icon-btn"
@@ -49,12 +48,11 @@
Cursor="Hand"/>
</Button>
<StackPanel Grid.Column="1" Spacing="0">
<TextBlock Text="{Binding TaskIdBadge}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}"
<TextBlock Classes="meta"
Text="{Binding TaskIdBadge}"
Margin="0,0,0,4"/>
<TextBox Text="{Binding EditableTitle, Mode=TwoWay}"
FontSize="14" FontWeight="Medium"
FontSize="{StaticResource FontSizeTaskTitle}" FontWeight="Medium"
BorderThickness="0" Background="Transparent"
Foreground="{DynamicResource TextBrush}"
TextWrapping="Wrap"
@@ -77,30 +75,31 @@
IsEnabled="{Binding IsAgentSectionEnabled}"
VerticalAlignment="Top"
Margin="6,0,0,0">
<TextBlock Text="⚙" FontSize="14"/>
<TextBlock Text="⚙" FontSize="{StaticResource FontSizeTaskTitle}"/>
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight" ShowMode="Standard">
<StackPanel Width="340" Spacing="10" Margin="4">
<TextBlock Text="Agent settings (overrides)" FontWeight="SemiBold"/>
<StackPanel Spacing="2">
<TextBlock Text="Model"/>
<TextBlock Classes="field-label" Text="Model"/>
<ComboBox ItemsSource="{Binding TaskModelOptions}"
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
<TextBlock Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6" FontSize="11"/>
<TextBlock Classes="meta"
Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6"/>
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Text="System prompt (appended)"/>
<TextBlock Classes="field-label" Text="System prompt (appended)"/>
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"
PlaceholderText="{Binding EffectiveSystemPromptHint}"/>
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Text="Agent file"/>
<TextBlock Classes="field-label" Text="Agent file"/>
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch">
@@ -110,8 +109,9 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6" FontSize="11"/>
<TextBlock Classes="meta"
Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6"/>
</StackPanel>
</StackPanel>
</Flyout>
@@ -128,39 +128,33 @@
<StackPanel Spacing="0">
<!-- Planning merge section — visible only for planning parent tasks -->
<Border Padding="18,12,18,12"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
<Border Classes="section-divider"
IsVisible="{Binding Task.IsPlanningParent}">
<StackPanel Spacing="8">
<TextBlock Classes="section-label" Text="MERGE" Margin="0,0,0,2"/>
<StackPanel Spacing="4">
<TextBlock Text="Merge target"
FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Classes="field-label" Text="Merge target"/>
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Review combined diff"
<Button Classes="btn" Content="Review combined diff"
Command="{Binding ReviewCombinedDiffCommand}"/>
<Button Content="Merge all subtasks"
<Button Classes="btn" Content="Merge all subtasks"
IsEnabled="{Binding CanMergeAll}"
Command="{Binding MergeAllCommand}"
ToolTip.Tip="{Binding MergeAllDisabledReason}"/>
</StackPanel>
<TextBlock Text="{Binding MergeAllError}"
Foreground="OrangeRed"
Foreground="{DynamicResource BloodBrush}"
TextWrapping="Wrap"
IsVisible="{Binding MergeAllError, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel>
</Border>
<!-- Steps section -->
<Border Padding="18,12,18,12"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Border Classes="section-divider">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="STEPS" Margin="0,0,0,2"/>
<TextBox Text="{Binding NewSubtaskTitle, Mode=TwoWay}"
@@ -169,7 +163,7 @@
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="6">
CornerRadius="8">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddSubtaskCommand}"/>
</TextBox.KeyBindings>
@@ -195,7 +189,7 @@
<TextBlock Grid.Column="1"
Classes="subtask-title"
Text="{Binding Title}"
FontSize="13"
FontSize="{StaticResource FontSizeBody}"
Foreground="{DynamicResource TextDimBrush}"
VerticalAlignment="Center"
TextWrapping="Wrap"/>
@@ -208,9 +202,7 @@
</Border>
<!-- Details (description) section -->
<Border Padding="18,12,18,12"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Border Classes="section-divider">
<StackPanel Spacing="6">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<Button Grid.Column="0"
@@ -220,12 +212,12 @@
Margin="0,0,6,2"
VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="▾" FontSize="10"
IsVisible="{Binding IsDescriptionExpanded}"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Text="▸" FontSize="10"
IsVisible="{Binding !IsDescriptionExpanded}"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Classes="meta"
Text="▾"
IsVisible="{Binding IsDescriptionExpanded}"/>
<TextBlock Classes="meta"
Text="▸"
IsVisible="{Binding !IsDescriptionExpanded}"/>
<TextBlock Classes="section-label" Text="DETAILS"/>
</StackPanel>
</Button>
@@ -239,19 +231,17 @@
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
</Button>
<Button Grid.Column="3"
Classes="icon-btn"
Classes="btn"
Command="{Binding ToggleEditDescriptionCommand}"
Padding="6,2"
FontSize="10"
Padding="8,3"
ToolTip.Tip="Toggle edit/preview"
IsVisible="{Binding IsDescriptionEditorVisible}">
<TextBlock Text="Preview"/>
</Button>
<Button Grid.Column="3"
Classes="icon-btn"
Classes="btn"
Command="{Binding ToggleEditDescriptionCommand}"
Padding="6,2"
FontSize="10"
Padding="8,3"
ToolTip.Tip="Toggle edit/preview"
IsVisible="{Binding IsDescriptionPreviewVisible}">
<TextBlock Text="Edit"/>
@@ -266,11 +256,11 @@
PlaceholderText="Add task details (markdown supported)..."
Padding="8"
FontFamily="{DynamicResource MonoFont}"
FontSize="12"
FontSize="{StaticResource FontSizeBody}"
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1"
CornerRadius="6"
CornerRadius="8"
IsVisible="{Binding IsDescriptionEditorVisible}"/>
<ctl:MarkdownView Markdown="{Binding EditableDescription}"

View File

@@ -4,20 +4,12 @@
xmlns:converters="using:ClaudeDo.Ui.Converters"
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<UserControl.Resources>
<converters:UpperCaseConverter x:Key="UpperCase"/>
<converters:IconKeyConverter x:Key="IconKey"/>
<converters:DotBrushConverter x:Key="DotBrush"/>
</UserControl.Resources>
<DockPanel LastChildFill="True">
<!-- ── Header ── -->
<Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Margin="14,12,14,0" Spacing="4">
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="18"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="Lists"/>
<StackPanel Spacing="4">
<TextBlock Classes="heading" Text="Lists"/>
<!-- Search row -->
<Border Classes="search-wrap" Margin="0,8,0,12">
@@ -45,18 +37,16 @@
<!-- Avatar circle -->
<Border Grid.Column="0" Classes="avatar-circle"
VerticalAlignment="Center">
<TextBlock Text="{Binding UserInitials}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
<TextBlock Classes="eyebrow"
Text="{Binding UserInitials}"
FontWeight="SemiBold"
Foreground="{DynamicResource DeepBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- Name + machine -->
<StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center">
<TextBlock Text="{Binding UserName}"
FontSize="12" Foreground="{DynamicResource TextBrush}"/>
<TextBlock FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}">
<TextBlock Classes="title" Text="{Binding UserName}"/>
<TextBlock Classes="meta">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} / local">
<Binding Path="MachineName"/>
@@ -80,7 +70,7 @@
<StackPanel Margin="6,0,6,4">
<!-- SMART LISTS section -->
<TextBlock Classes="list-section-label" Text="SMART LISTS"/>
<TextBlock Classes="section-label" Text="SMART LISTS" Margin="10,10,10,4"/>
<ItemsControl ItemsSource="{Binding SmartLists}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
@@ -90,10 +80,8 @@
<!-- Left accent bar for active state -->
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="Transparent"
CornerRadius="8" IsHitTestVisible="False">
<Border.IsVisible>
<Binding Path="IsActive"/>
</Border.IsVisible>
CornerRadius="8" IsHitTestVisible="False"
IsVisible="{Binding IsActive}">
<Border Width="2" Height="16"
Background="{DynamicResource AccentBrush}"
CornerRadius="1"
@@ -110,7 +98,7 @@
<TextBlock Grid.Column="1" Classes="list-label"
Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="13"/>
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count -->
<TextBlock Grid.Column="2" Classes="list-count"
Text="{Binding Count}"/>
@@ -121,7 +109,7 @@
</ItemsControl>
<!-- MY LISTS section -->
<TextBlock Classes="list-section-label" Text="MY LISTS"/>
<TextBlock Classes="section-label" Text="MY LISTS" Margin="10,10,10,4"/>
<ItemsControl ItemsSource="{Binding UserLists}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
@@ -158,7 +146,7 @@
<TextBlock Grid.Column="1" Classes="list-label"
Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="13"/>
Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count -->
<TextBlock Grid.Column="2" Classes="list-count"
Text="{Binding Count}"/>
@@ -177,7 +165,8 @@
Width="13" Height="13"
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="New list" FontSize="12"
<TextBlock Classes="body"
Text="New list"
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/>
</StackPanel>

View File

@@ -12,8 +12,8 @@
Height="28">
<!-- Session label -->
<TextBlock Grid.Column="1"
Classes="meta"
Text="{Binding BranchLine, StringFormat='claude-session · {0}'}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
LetterSpacing="0.8"
Foreground="{DynamicResource TextMuteBrush}"
HorizontalAlignment="Center"
@@ -65,7 +65,6 @@
<!-- Message text — selectable so the user can copy raw output -->
<SelectableTextBlock Grid.Column="1"
Text="{Binding Text}" Tag="{Binding ClassName}"
FontFamily="{DynamicResource MonoFont}" FontSize="11"
Foreground="{DynamicResource TextDimBrush}"
TextWrapping="Wrap"/>
</Grid>

View File

@@ -79,9 +79,9 @@
Width="18" Height="18"
VerticalAlignment="Center">
<Panel>
<TextBlock Text="▾" FontSize="10" IsVisible="{Binding IsExpanded}"
<TextBlock Classes="meta" Text="▾" IsVisible="{Binding IsExpanded}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="▸" FontSize="10" IsVisible="{Binding !IsExpanded}"
<TextBlock Classes="meta" Text="▸" IsVisible="{Binding !IsExpanded}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Panel>
</Button>
@@ -100,7 +100,7 @@
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0"
Classes="task-title"
Text="{Binding Title}" FontSize="14"
Text="{Binding Title}" FontSize="{StaticResource FontSizeTaskTitle}"
Foreground="{DynamicResource TextBrush}"
TextWrapping="Wrap"
FontStyle="{Binding IsDraft, Converter={StaticResource BoolToItalic}}"
@@ -218,24 +218,22 @@
<Button.Flyout>
<Flyout Placement="Bottom" ShowMode="Standard">
<Border Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1" CornerRadius="10"
Padding="16" Width="300">
<StackPanel Spacing="12">
<TextBlock Text="Schedule task"
FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource TextBrush}"/>
<TextBlock Classes="title" Text="Schedule task"/>
<StackPanel Spacing="6">
<TextBlock Text="WHEN" FontSize="10" Opacity="0.6"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Classes="eyebrow" Text="WHEN"
Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
<ctl:ThemedDatePicker x:Name="ScheduleDate" ShowTime="True"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,4,0,0">
<Button Content="Cancel" Click="OnScheduleCancelClick" MinWidth="76"/>
<Button Classes="btn" Content="Cancel" Click="OnScheduleCancelClick" MinWidth="76"/>
<Button Content="Schedule" Classes="accent" Click="OnScheduleSetClick" MinWidth="76"/>
</StackPanel>
</StackPanel>

View File

@@ -9,14 +9,13 @@
<!-- Header -->
<Border DockPanel.Dock="Top" Classes="island-header">
<Grid ColumnDefinitions="*,Auto" Margin="18,14">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="eyebrow" Text="{Binding HeaderEyebrow}"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="24"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
<TextBlock Classes="display"
Text="{Binding HeaderTitle}"
TextTrimming="CharacterEllipsis"/>
<TextBlock FontFamily="{DynamicResource MonoFamily}" FontSize="11"
<TextBlock Classes="meta"
Foreground="{DynamicResource TextMuteBrush}"
Text="{Binding Subtitle}"
TextTrimming="CharacterEllipsis"/>

View File

@@ -38,24 +38,18 @@
VerticalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
<!-- CLAUDEDO label -->
<TextBlock Classes="title-brand-name"
<TextBlock Classes="title-brand-name eyebrow"
Text="CLAUDEDO"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextBrush}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
<!-- separator dot -->
<TextBlock Text="·"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"
<TextBlock Classes="meta"
Text="·"
VerticalAlignment="Center"/>
<!-- current list name -->
<TextBlock Text="{Binding Lists.SelectedList.Name, Converter={StaticResource UpperCase}}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextDimBrush}"
<TextBlock Classes="meta"
Text="{Binding Lists.SelectedList.Name, Converter={StaticResource UpperCase}}"
LetterSpacing="1.4"
VerticalAlignment="Center"/>
<!-- Help menu -->
@@ -63,7 +57,7 @@
Background="Transparent"
VerticalAlignment="Center">
<MenuItem Header="Help"
FontSize="11"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource TextDimBrush}">
<MenuItem Header="Check for updates"
Command="{Binding CheckForUpdatesCommand}"/>
@@ -107,21 +101,20 @@
IsVisible="{Binding IsUpdateBannerVisible}">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Foreground="{DynamicResource TextDimBrush}"
FontSize="12">
Classes="body"
VerticalAlignment="Center">
<Run Text="Update available: v"/>
<Run Text="{Binding UpdateCheck.CurrentVersion}"/>
<Run Text=" → v"/>
<Run Text="{Binding UpdateBannerLatestVersion}"/>
</TextBlock>
<Button Grid.Column="1"
Classes="btn"
Margin="0,0,8,0"
Padding="10,3"
Content="Update now"
Command="{Binding UpdateNowCommand}"/>
<Button Grid.Column="2"
Padding="10,3"
Classes="btn"
Content="Dismiss"
Command="{Binding DismissBannerCommand}"/>
</Grid>
@@ -129,11 +122,10 @@
<!-- Inline update status (appears at right of banner row when no banner) -->
<TextBlock Grid.Row="1"
Classes="meta"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,14,0"
FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"
Text="{Binding InlineUpdateStatus}"
IsVisible="{Binding InlineUpdateStatus, Converter={x:Static ObjectConverters.IsNotNull}}"/>
@@ -194,39 +186,39 @@
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<DockPanel LastChildFill="True" Margin="14,0">
<!-- Left: connection pill -->
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" Spacing="7"
VerticalAlignment="Center">
<Ellipse Width="7" Height="7" Fill="#4CAF50"
<!-- Left: connection pill (click to open worker help) -->
<Button DockPanel.Dock="Left"
Command="{Binding OpenWorkerConnectionHelpCommand}"
Background="Transparent" BorderThickness="0" Padding="0"
Cursor="Hand" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusRunningBrush}"
IsVisible="{Binding Worker.IsConnected}"/>
<Ellipse Width="7" Height="7" Fill="#FFA726"
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusReviewBrush}"
IsVisible="{Binding Worker.IsReconnecting}"/>
<Ellipse Width="7" Height="7" Fill="#EF5350"
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusErrorBrush}"
IsVisible="{Binding IsOffline}"/>
<TextBlock Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
FontFamily="{DynamicResource MonoFont}"
FontSize="10"
<TextBlock Classes="eyebrow"
Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
LetterSpacing="1.4"
Foreground="{DynamicResource TextDimBrush}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!-- Right: worker log line -->
<TextBlock DockPanel.Dock="Right"
Classes="meta"
Text="{Binding WorkerLogText}"
IsVisible="{Binding IsWorkerLogVisible}"
Foreground="{Binding WorkerLogLevel, Converter={StaticResource WorkerLogLevelToBrush}}"
FontFamily="{DynamicResource MonoFont}"
FontSize="10"
LetterSpacing="1.4"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
<!-- Right: prime status notification -->
<TextBlock DockPanel.Dock="Right"
Classes="meta"
Text="{Binding PrimeStatus}"
Foreground="{DynamicResource TextDimBrush}"
FontSize="11"
VerticalAlignment="Center"
Margin="12,0,0,0"
IsVisible="{Binding PrimeStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>

View File

@@ -68,6 +68,12 @@ public partial class MainWindow : Window
modal.CloseAction = () => dlg.Close();
await dlg.ShowDialog(this);
};
vm.ShowWorkerConnectionModal = async (connVm) =>
{
var dlg = new WorkerConnectionModalView { DataContext = connVm };
connVm.CloseAction = () => dlg.Close();
await dlg.ShowDialog(this);
};
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.AboutModalView"
x:DataType="vm:AboutModalViewModel"
Title="About ClaudeDo"
@@ -12,38 +13,24 @@
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1">
<Grid RowDefinitions="36,*,52">
<Border Grid.Row="0" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="ABOUT" FontFamily="{DynamicResource MonoFont}" FontSize="11"
LetterSpacing="1.4" Foreground="{DynamicResource TextBrush}" VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕" FontSize="12"
Command="{Binding CloseCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" Padding="20,16" HorizontalScrollBarVisibility="Disabled">
<ctl:ModalShell Title="ABOUT" CloseCommand="{Binding CloseCommand}">
<!-- Body -->
<ScrollViewer Padding="20,16" HorizontalScrollBarVisibility="Disabled">
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="10" ColumnSpacing="8">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version" Foreground="{DynamicResource TextDimBrush}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding AppVersion}" FontFamily="{DynamicResource MonoFont}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Data" Foreground="{DynamicResource TextDimBrush}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding DataFolderPath}" FontFamily="{DynamicResource MonoFont}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<Button Grid.Row="1" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding DataFolderPath}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Logs" Foreground="{DynamicResource TextDimBrush}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding LogsFolderPath}" FontFamily="{DynamicResource MonoFont}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<Button Grid.Row="2" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding LogsFolderPath}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Config" Foreground="{DynamicResource TextDimBrush}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding WorkerConfigPath}" FontFamily="{DynamicResource MonoFont}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<Button Grid.Row="3" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding WorkerConfigPath}"/>
<TextBlock Classes="meta" Grid.Row="0" Grid.Column="0" Text="Version" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="0" Grid.Column="1" Text="{Binding AppVersion}" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="1" Grid.Column="0" Text="Data" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="1" Grid.Column="1" Text="{Binding DataFolderPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding DataFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="2" Grid.Column="0" Text="Logs" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="2" Grid.Column="1" Text="{Binding LogsFolderPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding LogsFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="3" Grid.Column="0" Text="Config" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="3" Grid.Column="1" Text="{Binding WorkerConfigPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="3" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding WorkerConfigPath}"/>
</Grid>
</ScrollViewer>
<Border Grid.Row="2" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="16,0">
<Button Content="Close" Command="{Binding CloseCommand}" MinWidth="90"/>
</StackPanel>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.DiffModalView"
x:DataType="vm:DiffModalViewModel"
Title="Diff"
@@ -8,7 +9,7 @@
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{StaticResource SurfaceBrush}">
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
@@ -17,10 +18,10 @@
<Window.Styles>
<!-- diff line row tints via Tag selector (compiled-binding-friendly) -->
<Style Selector="Border.diff-line[Tag=add]">
<Setter Property="Background" Value="#1A4A6B4A"/>
<Setter Property="Background" Value="{StaticResource RunningTintBrush}"/>
</Style>
<Style Selector="Border.diff-line[Tag=del]">
<Setter Property="Background" Value="#1AC87060"/>
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}"/>
</Style>
<Style Selector="Border.diff-line[Tag=ctx]">
<Setter Property="Background" Value="Transparent"/>
@@ -45,44 +46,20 @@
</Style>
</Window.Styles>
<!-- Outer container — rectangular so the OS window rectangle stays filled (no black corners) -->
<Border Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*">
<!-- Title bar / drag handle -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{StaticResource Surface2Brush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="Diff" VerticalAlignment="Center"
FontFamily="{StaticResource MonoFamily}"
FontSize="12"
Foreground="{StaticResource TextDimBrush}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
<Button Content="Merge…"
Command="{Binding MergeCommand}"
Margin="0,0,4,0" />
<Button Classes="icon-btn"
Content="✕"
FontSize="12"
Command="{Binding CloseCommand}" />
<ctl:ModalShell Title="DIFF" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Merge…" Command="{Binding MergeCommand}"/>
</StackPanel>
</Grid>
</Border>
</ctl:ModalShell.Footer>
<!-- Body: sidebar + diff content -->
<Grid Grid.Row="1" ColumnDefinitions="240,*">
<Grid ColumnDefinitions="240,*">
<!-- File sidebar -->
<Border Grid.Column="0"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="0,0,1,0"
Background="{StaticResource DeepBrush}">
Classes="sidebar-pane">
<ListBox ItemsSource="{Binding Files}"
SelectedItem="{Binding SelectedFile, Mode=TwoWay}"
Background="Transparent"
@@ -92,23 +69,16 @@
<DataTemplate x:DataType="vm:DiffFileViewModel">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="4">
<TextBlock Text="{Binding Path}"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"
Foreground="{StaticResource TextDimBrush}"
<TextBlock Classes="path-mono" Text="{Binding Path}"
TextTrimming="PrefixCharacterEllipsis"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Classes="chip" Padding="5,2">
<TextBlock Foreground="{StaticResource MossBrightBrush}"
FontFamily="{StaticResource MonoFamily}"
FontSize="10"
<TextBlock Foreground="{DynamicResource MossBrightBrush}"
Text="{Binding Additions, StringFormat='+{0}'}"/>
</Border>
<Border Classes="chip" Padding="5,2">
<TextBlock Foreground="{StaticResource BloodBrush}"
FontFamily="{StaticResource MonoFamily}"
FontSize="10"
Text="{Binding Deletions, StringFormat='\u2212{0}'}"/>
<TextBlock Foreground="{DynamicResource BloodBrush}"
Text="{Binding Deletions, StringFormat='{0}'}"/>
</Border>
</StackPanel>
</StackPanel>
@@ -119,14 +89,11 @@
</Border>
<!-- Diff content -->
<Grid Grid.Column="1" Background="{StaticResource VoidBrush}">
<TextBlock Text="{Binding StatusMessage}"
<Grid Grid.Column="1" Background="{DynamicResource VoidBrush}">
<TextBlock Classes="body" Text="{Binding StatusMessage}"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource TextDimBrush}"
FontFamily="{StaticResource MonoFamily}"
FontSize="12"/>
VerticalAlignment="Center"/>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding SelectedFile.Lines}">
@@ -140,32 +107,26 @@
<TextBlock Grid.Column="0"
Text="{Binding OldNo}"
Classes="diff-lineno"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"
Foreground="{StaticResource TextFaintBrush}"
HorizontalAlignment="Right"
Margin="0,0,8,0"/>
<!-- New line number -->
<TextBlock Grid.Column="1"
Text="{Binding NewNo}"
Classes="diff-lineno"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"
Foreground="{StaticResource TextFaintBrush}"
HorizontalAlignment="Right"
Margin="0,0,8,0"/>
<!-- Sign -->
<TextBlock Grid.Column="2"
Classes="diff-sign"
Text="{Binding Sign}"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"/>
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeMono}"/>
<!-- Line text -->
<TextBlock Grid.Column="3"
Classes="diff-text"
Text="{Binding Text}"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeMono}"
TextWrapping="NoWrap"/>
</Grid>
</Border>
@@ -175,6 +136,5 @@
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -2,7 +2,6 @@ using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Styling;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -43,10 +42,4 @@ public partial class DiffModalView : Window
Opacity = 1;
RenderTransform = new ScaleTransform(1.0, 1.0);
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView"
x:DataType="vm:ListSettingsModalViewModel"
Title="List settings"
@@ -17,68 +18,21 @@
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
</Window.KeyBindings>
<Window.Styles>
<Style Selector="TextBlock.section-label">
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="LetterSpacing" Value="1.4"/>
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/>
<Setter Property="Margin" Value="4,0,0,6"/>
</Style>
<Style Selector="TextBlock.field-label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style Selector="Border.section">
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="14"/>
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource BloodBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*,52">
<!-- Title bar -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="LIST SETTINGS"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1"
Classes="icon-btn"
Content="✕"
FontSize="12"
Command="{Binding CancelCommand}"
VerticalAlignment="Center"/>
<ctl:ModalShell Title="LIST SETTINGS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Button Grid.Column="0" Content="Delete list" Classes="danger"
Command="{Binding DeleteCommand}" MinWidth="90"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
</StackPanel>
</Grid>
</Border>
</ctl:ModalShell.Footer>
<!-- Body -->
<ScrollViewer Grid.Row="1" Padding="20,16">
<StackPanel Spacing="18">
<ScrollViewer Padding="20,16">
<StackPanel Spacing="12">
<!-- GENERAL -->
<StackPanel Spacing="0">
@@ -94,7 +48,7 @@
<TextBlock Classes="field-label" Text="Working directory"/>
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="(none)" />
<Button Grid.Column="1" Content="Browse..." Margin="8,0,0,0" Click="BrowseClicked" />
<Button Classes="btn" Grid.Column="1" Content="Browse..." Margin="8,0,0,0" Click="BrowseClicked" />
</Grid>
</StackPanel>
@@ -112,7 +66,7 @@
<StackPanel Spacing="0">
<Grid ColumnDefinitions="*,Auto" Margin="4,0,0,6">
<TextBlock Classes="section-label" Text="AGENT" Margin="0"/>
<Button Grid.Column="1" Content="Reset agent settings"
<Button Classes="btn" Grid.Column="1" Content="Reset agent settings"
Command="{Binding ResetAgentSettingsCommand}" />
</Grid>
<Border Classes="section">
@@ -141,22 +95,16 @@
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"
Foreground="{DynamicResource TextBrush}"/>
<TextBlock Text="{Binding Description}"
Foreground="{DynamicResource TextMuteBrush}"
FontSize="11" />
<TextBlock Classes="title" Text="{Binding Name}"/>
<TextBlock Classes="meta" Text="{Binding Description}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="1" Content="Browse..."
<Button Classes="btn" Grid.Column="1" Content="Browse..."
Margin="8,0,0,0" Click="BrowseAgentClicked" />
</Grid>
<TextBlock Text="{Binding SelectedAgent.Path}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"
<TextBlock Classes="path-mono" Text="{Binding SelectedAgent.Path}"
TextTrimming="PrefixCharacterEllipsis"
IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
@@ -167,21 +115,5 @@
</StackPanel>
</ScrollViewer>
<!-- Footer -->
<Border Grid.Row="2"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center" Margin="16,0">
<Button Grid.Column="0" Content="Delete list" Classes="danger"
Command="{Binding DeleteCommand}" MinWidth="90"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Data.Models;
@@ -14,12 +13,6 @@ public partial class ListSettingsModalView : Window
InitializeComponent();
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private async void BrowseAgentClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is not ListSettingsModalViewModel vm) return;

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.MergeModalView"
x:DataType="vm:MergeModalViewModel"
Title="Merge worktree"
@@ -15,54 +16,22 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<Window.Styles>
<Style Selector="TextBlock.field-label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*,52">
<!-- Title bar -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="MERGE WORKTREE"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1"
Classes="icon-btn"
Content="✕"
FontSize="12"
Command="{Binding CancelCommand}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<ctl:ModalShell Title="MERGE WORKTREE" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Merge" Classes="primary"
Command="{Binding SubmitCommand}"
IsDefault="True" MinWidth="90"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Body -->
<ScrollViewer Grid.Row="1" Padding="20,16">
<ScrollViewer Padding="20,16">
<StackPanel Spacing="12">
<TextBlock Text="{Binding TaskTitle, StringFormat='Merging: {0}'}"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}" />
<TextBlock Classes="title" Text="{Binding TaskTitle, StringFormat='Merging: {0}'}" />
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Target branch"/>
@@ -90,22 +59,14 @@
TextWrapping="Wrap"
IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Border BorderBrush="{DynamicResource BloodBrush}"
BorderThickness="1"
CornerRadius="6"
Padding="12,10"
<Border Classes="danger-box"
IsVisible="{Binding HasConflict}">
<StackPanel Spacing="4">
<TextBlock Text="Conflicted files:"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}" />
<TextBlock Classes="title" Text="Conflicted files:" />
<ItemsControl ItemsSource="{Binding ConflictFiles}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextDimBrush}" />
<TextBlock Classes="meta" Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -118,21 +79,5 @@
</StackPanel>
</ScrollViewer>
<!-- Footer -->
<Border Grid.Row="2"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="16,0">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Merge" Classes="primary"
Command="{Binding SubmitCommand}"
IsDefault="True" MinWidth="90"/>
</StackPanel>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
@@ -18,9 +17,5 @@ public partial class MergeModalView : Window
};
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.RepoImportModalView"
x:DataType="vm:RepoImportModalViewModel"
Title="Add repos as lists"
@@ -12,34 +13,33 @@
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1">
<Grid RowDefinitions="36,Auto,*,52">
<!-- Header -->
<Border Grid.Row="0" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="ADD REPOS AS LISTS" FontFamily="{DynamicResource MonoFont}" FontSize="11"
LetterSpacing="1.4" Foreground="{DynamicResource TextBrush}" VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕" FontSize="12"
Command="{Binding CancelCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
<ctl:ModalShell Title="ADD REPOS AS LISTS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"
VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{Binding CreateButtonText}" Command="{Binding CreateCommand}"
IsEnabled="{Binding CanCreate}" MinWidth="120" Classes="primary"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Body: toolbar + checklist -->
<DockPanel>
<!-- Toolbar: search + folder actions -->
<StackPanel Grid.Row="1" Spacing="8" Margin="16,12,16,6">
<StackPanel DockPanel.Dock="Top" Spacing="8" Margin="20,12,20,6">
<TextBox Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Search repos…"/>
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Grid.Column="0" Content="Add folder…" Click="AddFolderClicked"/>
<Button Grid.Column="2" Content="Forget folders"
<Button Classes="btn" Grid.Column="0" Content="Add folder…" Click="AddFolderClicked"/>
<Button Classes="btn" Grid.Column="2" Content="Forget folders"
Command="{Binding ForgetFoldersCommand}"
IsVisible="{Binding HasFolders}"/>
</Grid>
</StackPanel>
<!-- Repo checklist -->
<ScrollViewer Grid.Row="2" Padding="16,2,16,8">
<ScrollViewer Padding="20,2,20,8">
<ItemsControl ItemsSource="{Binding Repos}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:RepoImportItemViewModel">
@@ -49,16 +49,11 @@
IsChecked="{Binding IsChecked, Mode=TwoWay}"
IsEnabled="{Binding CanToggle}"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"
Foreground="{DynamicResource TextBrush}" FontSize="12"
<TextBlock Classes="body" Grid.Column="1" Text="{Binding Name}"
VerticalAlignment="Center" Margin="4,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding FullPath}"
Foreground="{DynamicResource TextFaintBrush}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
TextTrimming="CharacterEllipsis"
<TextBlock Classes="path-mono" Grid.Column="2" Text="{Binding FullPath}"
VerticalAlignment="Center" Margin="8,0,0,0"/>
<TextBlock Grid.Column="3" Text="(already added)"
Foreground="{DynamicResource TextFaintBrush}" FontSize="10"
<TextBlock Classes="meta" Grid.Column="3" Text="(already added)"
VerticalAlignment="Center" Margin="8,0,0,0"
IsVisible="{Binding AlreadyAdded}"/>
</Grid>
@@ -66,18 +61,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<!-- Footer -->
<Border Grid.Row="3" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="16,0">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{Binding CreateButtonText}" Command="{Binding CreateCommand}"
IsEnabled="{Binding CanCreate}" MinWidth="120" Classes="primary"/>
</StackPanel>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -22,71 +22,28 @@
<conv:TimeSpanToHhmmConverter x:Key="TimeSpanToHhmm"/>
</Window.Resources>
<Window.Styles>
<Style Selector="TextBlock.section-label">
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="LetterSpacing" Value="1.4"/>
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/>
<Setter Property="Margin" Value="4,0,0,6"/>
</Style>
<Style Selector="TextBlock.field-label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style Selector="TextBlock.path-mono">
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource BloodBrush}"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*,52">
<!-- Title bar -->
<Border Grid.Row="0" x:Name="TitleBar"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="SETTINGS"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕" FontSize="12"
Command="{Binding CancelCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
<ctl:ModalShell Title="SETTINGS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary"
Command="{Binding SaveCommand}"
IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Body: tabs + bottom validation/status strip -->
<DockPanel Grid.Row="1">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2">
<TextBlock Text="{Binding ValidationError}"
Foreground="{DynamicResource BloodBrush}" FontSize="11"
<TextBlock Classes="meta" Text="{Binding ValidationError}"
Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding ValidationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
<TextBlock Classes="meta" Text="{Binding StatusMessage}"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<TabControl Padding="20,12" TabStripPlacement="Top">
<TabControl Padding="20,16" TabStripPlacement="Top">
<TabItem Header="General">
<ScrollViewer>
@@ -143,11 +100,11 @@
<NumericUpDown Value="{Binding Worktrees.WorktreeAutoCleanupDays, Mode=TwoWay}"
Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0"
IsEnabled="{Binding Worktrees.WorktreeAutoCleanupEnabled}"/>
<TextBlock Text="days" VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Classes="body" Text="days" VerticalAlignment="Center"/>
</StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
<StackPanel Spacing="8">
<Button Content="Cleanup finished worktrees"
<Button Classes="btn" Content="Cleanup finished worktrees"
Command="{Binding Worktrees.CleanupWorktreesCommand}"
HorizontalAlignment="Left"/>
<StackPanel>
@@ -155,14 +112,13 @@
Command="{Binding Worktrees.RequestResetConfirmCommand}"
HorizontalAlignment="Left"
IsVisible="{Binding !Worktrees.ShowResetConfirm}"/>
<Border BorderBrush="{DynamicResource BloodBrush}" BorderThickness="1"
CornerRadius="6" Padding="12,10"
<Border Classes="danger-box"
IsVisible="{Binding Worktrees.ShowResetConfirm}">
<StackPanel Spacing="8">
<TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost."
Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Cancel" Command="{Binding Worktrees.CancelResetConfirmCommand}"/>
<Button Classes="btn" Content="Cancel" Command="{Binding Worktrees.CancelResetConfirmCommand}"/>
<Button Content="Remove All" Classes="danger"
Command="{Binding Worktrees.ConfirmResetAllCommand}"/>
</StackPanel>
@@ -170,8 +126,7 @@
</Border>
</StackPanel>
</StackPanel>
<TextBlock Text="{Binding Worktrees.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
<TextBlock Classes="meta" Text="{Binding Worktrees.StatusMessage}"
IsVisible="{Binding Worktrees.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ScrollViewer>
@@ -179,13 +134,12 @@
<TabItem Header="Files">
<ScrollViewer>
<StackPanel Spacing="14" Margin="0,8,0,0">
<StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="AGENTS"/>
<TextBlock Text="Restore bundled default agents. Existing files are not overwritten."
FontSize="11" TextWrapping="Wrap"
Foreground="{DynamicResource TextDimBrush}"/>
<Button Content="Restore default agents"
<TextBlock Classes="meta" Text="Restore bundled default agents. Existing files are not overwritten."
TextWrapping="Wrap"/>
<Button Classes="btn" Content="Restore default agents"
Command="{Binding Files.RestoreDefaultAgentsCommand}"
IsEnabled="{Binding !Files.IsBusy}"
HorizontalAlignment="Left"/>
@@ -195,20 +149,19 @@
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="System" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="0" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="0" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="System"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Planning" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="1" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Planning"/>
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Agent" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.AgentPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="2" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Agent"/>
</Grid>
</StackPanel>
<TextBlock Text="{Binding Files.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
<TextBlock Classes="meta" Text="{Binding Files.StatusMessage}"
IsVisible="{Binding Files.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ScrollViewer>
@@ -217,8 +170,7 @@
<TabItem Header="Prime Claude">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<TextBlock TextWrapping="Wrap" FontSize="11"
Foreground="{DynamicResource TextDimBrush}"
<TextBlock Classes="meta" TextWrapping="Wrap"
Text="Prime your Claude usage window each morning by firing a single non-interactive ping at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately."/>
<ItemsControl ItemsSource="{Binding Prime.Rows}">
<ItemsControl.ItemTemplate>
@@ -239,10 +191,9 @@
VerticalAlignment="Center"/>
<CheckBox Grid.Column="3" Content="MonFri"
IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="4" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
<TextBlock Classes="meta" Grid.Column="4" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
MinWidth="80"/>
<Button Grid.Column="5" Content="✕"
<Button Classes="icon-btn" Grid.Column="5" Content="✕"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
CommandParameter="{Binding}"/>
</Grid>
@@ -250,7 +201,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
<Button Classes="btn" Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
</StackPanel>
</ScrollViewer>
</TabItem>
@@ -258,21 +209,5 @@
</TabControl>
</DockPanel>
<!-- Footer -->
<Border Grid.Row="2"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="16,0">
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary"
Command="{Binding SaveCommand}"
IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
</StackPanel>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
@@ -17,10 +16,4 @@ public partial class SettingsModalView : Window
if (DataContext is SettingsModalViewModel vm)
vm.CloseAction = Close;
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.UnfinishedPlanningModalView"
x:DataType="vm:UnfinishedPlanningModalViewModel"
Title="Unfinished planning session"
@@ -15,68 +16,25 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<Window.Styles>
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</Window.Styles>
<Border Background="{DynamicResource SurfaceBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*,52">
<!-- Title bar -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="UNFINISHED PLANNING SESSION"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1"
Classes="icon-btn"
Content="✕"
FontSize="12"
Command="{Binding CancelCommand}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<ctl:ModalShell Title="UNFINISHED PLANNING SESSION" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Discard" Command="{Binding DiscardCommand}" MinWidth="80"/>
<Button Classes="btn" Content="Finalize" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
<Button Content="Resume" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Body -->
<StackPanel Grid.Row="1" Margin="20,16" Spacing="8">
<TextBlock Text="{Binding TaskTitle}"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}"
<StackPanel Margin="20,16" Spacing="8">
<TextBlock Classes="title" Text="{Binding TaskTitle}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Foreground="{DynamicResource TextDimBrush}">
<TextBlock Classes="body">
<Run Text="{Binding DraftCount}"/>
<Run Text=" draft task(s) waiting to be finalized."/>
</TextBlock>
</StackPanel>
<!-- Footer -->
<Border Grid.Row="2"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="16,0">
<Button Content="Discard" Command="{Binding DiscardCommand}" MinWidth="80"/>
<Button Content="Finalize" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
<Button Content="Resume" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
</StackPanel>
</Border>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace ClaudeDo.Ui.Views.Modals;
@@ -14,10 +13,4 @@ public partial class UnfinishedPlanningModalView : Window
vm.CloseAction = () => Close();
};
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -0,0 +1,29 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.WorkerConnectionModalView"
x:DataType="vm:WorkerConnectionModalViewModel"
Title="Worker not reachable"
Width="520" Height="240"
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="WORKER NOT REACHABLE" CloseCommand="{Binding CloseCommand}">
<Grid RowDefinitions="*,Auto" Margin="20,16">
<TextBlock Grid.Row="0" Classes="meta" TextWrapping="Wrap"
Text="ClaudeDo can't reach the background worker. It is normally started automatically at logon. You can start it now, or reinstall if the problem persists."/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,16,0,0">
<Button Classes="btn" Content="Dismiss" Command="{Binding CloseCommand}"/>
<Button Classes="btn" Content="Rerun Installer" Command="{Binding RerunInstallerCommand}"/>
<Button Classes="btn primary" Content="Start Worker" Command="{Binding StartWorkerCommand}"/>
</StackPanel>
</Grid>
</ctl:ModalShell>
</Window>

View File

@@ -0,0 +1,10 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ClaudeDo.Ui.Views.Modals;
public partial class WorkerConnectionModalView : Window
{
public WorkerConnectionModalView() => InitializeComponent();
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}

View File

@@ -31,7 +31,7 @@
PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Grid.Column="0" Text="Worktree" VerticalAlignment="Center"
FontFamily="{DynamicResource MonoFont}" FontSize="12"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeBody}"
Foreground="{DynamicResource TextMuteBrush}"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
Command="{Binding CloseCommand}" VerticalAlignment="Center"/>
@@ -40,10 +40,7 @@
<!-- Path strip -->
<Border DockPanel.Dock="Top" Padding="14,0,14,8">
<TextBlock Text="{Binding WorktreePath}"
FontFamily="{DynamicResource MonoFont}" FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Classes="path-mono" Text="{Binding WorktreePath}"/>
</Border>
<!-- Split: file tree | splitter | diff pane -->
@@ -66,14 +63,12 @@
ItemsSource="{Binding Children}">
<Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Name}"
FontFamily="{DynamicResource MonoFont}" FontSize="12"
Foreground="{DynamicResource TextBrush}"/>
<TextBlock Classes="meta" Text="{Binding Name}"/>
<Border Tag="{Binding Status}" CornerRadius="3" Padding="4,0"
VerticalAlignment="Center"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Status}"
FontFamily="{DynamicResource MonoFont}" FontSize="10"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
</StackPanel>
@@ -95,7 +90,7 @@
<DataTemplate DataType="vm:WorktreeDiffLineViewModel">
<SelectableTextBlock Text="{Binding Text}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
FontSize="{StaticResource FontSizeMono}"
Foreground="{Binding Kind, Converter={StaticResource DiffLineKindToBrush}}"
TextWrapping="NoWrap"/>
</DataTemplate>

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.WorktreesOverviewModalView"
x:DataType="vm:WorktreesOverviewModalViewModel"
Title="{Binding Title}"
@@ -10,13 +11,12 @@
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1">
ExtendClientAreaToDecorationsHint="True">
<Window.Resources>
<converters:WorktreeStateColorConverter x:Key="WorktreeStateColor"/>
<DataTemplate x:Key="WorktreeRowTemplate" x:DataType="vm:WorktreeOverviewRowViewModel">
<Border Classes="wt-row"
<Border Classes="task-row"
Classes.selected="{Binding IsSelected}"
Tapped="OnRowTapped">
<Border.ContextMenu>
@@ -53,120 +53,63 @@
CommandParameter="{Binding}"/>
<Separator/>
<MenuItem Header="Force remove"
Foreground="#EF5350"
Foreground="{DynamicResource StatusErrorBrush}"
Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ForceRemoveCommand}"
CommandParameter="{Binding}"/>
</ContextMenu>
</Border.ContextMenu>
<Grid ColumnDefinitions="*,90,80,80">
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="2">
<TextBlock Text="{Binding TaskTitle}" FontWeight="SemiBold"/>
<TextBlock Classes="title" Text="{Binding TaskTitle}"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding TaskStatus}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Text="•" FontSize="10" Foreground="{DynamicResource TextFaintBrush}"
<TextBlock Classes="meta" Text="{Binding TaskStatus}"/>
<TextBlock Classes="meta" Text="•"
IsVisible="{Binding !PathExistsOnDisk}"/>
<TextBlock Text="phantom" FontSize="10" Foreground="#EF5350"
<TextBlock Classes="meta" Text="phantom" Foreground="{DynamicResource StatusErrorBrush}"
IsVisible="{Binding !PathExistsOnDisk}"
ToolTip.Tip="Directory missing on disk"/>
</StackPanel>
</StackPanel>
<Border Grid.Column="1" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
<TextBlock Text="{Binding State}" FontSize="10" Foreground="White"
<TextBlock Classes="meta" Text="{Binding State}" Foreground="{DynamicResource TextBrush}"
HorizontalAlignment="Center"/>
</Border>
<TextBlock Grid.Column="2" Text="{Binding DiffStat}" VerticalAlignment="Center"
FontFamily="{DynamicResource MonoFont}" FontSize="11"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Grid.Column="3" Text="{Binding AgeText}" VerticalAlignment="Center"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Grid.Column="2" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Window.Styles>
<Style Selector="Border.wt-row">
<Setter Property="Padding" Value="12,10"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,0,0,6"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.10"/>
</Transitions>
</Setter>
</Style>
<Style Selector="Border.wt-row:pointerover">
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
</Style>
<Style Selector="Border.wt-row.selected">
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
</Style>
</Window.Styles>
<ctl:ModalShell Title="{Binding Title}" CloseCommand="{Binding CloseCommand}">
<Grid RowDefinitions="36,Auto,*,52">
<!-- Title bar -->
<Border Grid.Row="0"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="{Binding Title}"
VerticalAlignment="Center"
Margin="14,0,0,0"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}"/>
<Button Grid.Column="1"
Content="✕"
Command="{Binding CloseCommand}"
Margin="0,0,8,0"
Width="28" Height="28"
FontSize="11"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"/>
</Grid>
</Border>
<!-- Body: toolbar + content -->
<DockPanel>
<!-- Toolbar -->
<Border Grid.Row="1"
<Border DockPanel.Dock="Top"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1"
Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Content="Cleanup finished" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Classes="btn" Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Classes="btn" Content="Cleanup finished" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="12,0,0,0"
Foreground="{DynamicResource TextDimBrush}"/>
</StackPanel>
</Border>
<!-- Content -->
<ScrollViewer Grid.Row="2" Padding="12,8">
<ScrollViewer Padding="20,16">
<StackPanel>
<!-- Column headers -->
<Grid ColumnDefinitions="*,90,80,80" Margin="12,0,12,4">
<TextBlock Grid.Column="0" Text="TASK"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="1" Text="STATE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="2" Text="DIFF"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="3" Text="AGE"
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4"
Foreground="{DynamicResource TextFaintBrush}"/>
<TextBlock Grid.Column="0" Classes="eyebrow" Text="TASK"/>
<TextBlock Grid.Column="1" Classes="eyebrow" Text="STATE"/>
<TextBlock Grid.Column="2" Classes="eyebrow" Text="DIFF"/>
<TextBlock Grid.Column="3" Classes="eyebrow" Text="AGE"/>
</Grid>
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8"/>
@@ -183,7 +126,8 @@
<ItemsControl ItemsSource="{Binding Groups}" IsVisible="{Binding IsGlobal}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreesGroupViewModel">
<Expander Header="{Binding ListName}" IsExpanded="True" Margin="0,0,0,6">
<Expander Header="{Binding ListName}" IsExpanded="True" Margin="0,0,0,6"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<ItemsControl ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
@@ -198,16 +142,7 @@
</StackPanel>
</ScrollViewer>
<!-- Footer -->
<Border Grid.Row="3"
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
Padding="12,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Command="{Binding CloseCommand}"/>
</StackPanel>
</Border>
</DockPanel>
</Grid>
</ctl:ModalShell>
</Window>

View File

@@ -17,9 +17,5 @@ public partial class WorktreesOverviewModalView : Window
}
}
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Planning"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:DataType="vm:ConflictResolutionViewModel"
x:Class="ClaudeDo.Ui.Views.Planning.ConflictResolutionView"
Title="Merge conflict"
@@ -8,58 +9,41 @@
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{StaticResource SurfaceBrush}">
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding AbortCommand}"/>
</Window.KeyBindings>
<Border Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,*">
<!-- Title bar / drag handle -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{StaticResource Surface2Brush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="Merge conflict"
VerticalAlignment="Center"
FontFamily="{StaticResource MonoFamily}"
FontSize="12"
Foreground="{StaticResource TextDimBrush}"/>
</Grid>
</Border>
<ctl:ModalShell Title="MERGE CONFLICT" CloseCommand="{Binding AbortCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Open all in VS Code" Command="{Binding OpenInVsCodeCommand}"/>
<Button Classes="btn" Content="I've resolved — continue" Command="{Binding ContinueCommand}"/>
<Button Classes="btn" Content="Abort this merge" Command="{Binding AbortCommand}"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Content -->
<StackPanel Grid.Row="1" Spacing="12" Margin="16" MinWidth="520">
<TextBlock FontWeight="SemiBold" FontSize="16"
<StackPanel Spacing="12" Margin="20,16" MinWidth="520">
<TextBlock Classes="heading"
Text="{Binding SubtaskTitle, StringFormat='Conflicts in subtask: {0}'}"/>
<TextBlock Text="{Binding TargetBranch, StringFormat='Merging into: {0}'}" Opacity="0.7"/>
<TextBlock Classes="body" Text="{Binding TargetBranch, StringFormat='Merging into: {0}'}"/>
<ItemsControl ItemsSource="{Binding ConflictedFiles}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontFamily="Consolas,Menlo,monospace"/>
<TextBlock Classes="path-mono" Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding VsCodeError}" Foreground="OrangeRed"
<TextBlock Classes="meta" Text="{Binding VsCodeError}" Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding VsCodeError, Converter={x:Static ObjectConverters.IsNotNull}}"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding ActionError}" Foreground="OrangeRed"
<TextBlock Classes="meta" Text="{Binding ActionError}" Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding ActionError, Converter={x:Static ObjectConverters.IsNotNull}}"
TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,4,0,4">
<Button Content="Open all in VS Code" Command="{Binding OpenInVsCodeCommand}"/>
<Button Content="I've resolved — continue" Command="{Binding ContinueCommand}"/>
<Button Content="Abort this merge" Command="{Binding AbortCommand}"/>
</StackPanel>
</StackPanel>
</Grid>
</Border>
</ctl:ModalShell>
</Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Planning;
namespace ClaudeDo.Ui.Views.Planning;
@@ -17,10 +16,4 @@ public partial class ConflictResolutionView : Window
if (DataContext is ConflictResolutionViewModel vm)
vm.CloseRequested = Close;
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -1,6 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Planning"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Planning.PlanningDiffView"
x:DataType="vm:PlanningDiffViewModel"
Title="Planning — Combined diff"
@@ -8,47 +9,23 @@
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{StaticResource SurfaceBrush}">
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<Border Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="1">
<Grid RowDefinitions="36,Auto,*">
<!-- Title bar / drag handle -->
<Border Grid.Row="0"
x:Name="TitleBar"
Background="{StaticResource Surface2Brush}"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_PointerPressed">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Text="Planning — Combined diff"
VerticalAlignment="Center"
FontFamily="{StaticResource MonoFamily}"
FontSize="12"
Foreground="{StaticResource TextDimBrush}"/>
<Button Grid.Column="1"
Classes="icon-btn"
Content="✕"
FontSize="12"
Command="{Binding CloseCommand}"
VerticalAlignment="Center"/>
</Grid>
</Border>
<ctl:ModalShell Title="PLANNING — COMBINED DIFF" CloseCommand="{Binding CloseCommand}">
<!-- Toolbar row -->
<StackPanel Grid.Row="1"
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Spacing="8"
Margin="8,6">
<ToggleButton Content="Preview combined" IsChecked="{Binding IsCombinedMode}"/>
<TextBlock Text="{Binding CombinedWarning}"
Foreground="Orange"
Foreground="{DynamicResource BloodBrush}"
VerticalAlignment="Center"
IsVisible="{Binding CombinedWarning, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock Text="Loading…"
@@ -57,13 +34,11 @@
</StackPanel>
<!-- Two-pane body -->
<Grid Grid.Row="2" ColumnDefinitions="240,*">
<Grid ColumnDefinitions="240,*">
<!-- Subtask list (left pane) -->
<Border Grid.Column="0"
BorderBrush="{StaticResource LineBrush}"
BorderThickness="0,0,1,0"
Background="{StaticResource DeepBrush}">
Classes="sidebar-pane">
<ListBox ItemsSource="{Binding Subtasks}"
SelectedItem="{Binding SelectedSubtask}"
IsEnabled="{Binding !IsCombinedMode}"
@@ -74,13 +49,9 @@
<DataTemplate x:DataType="vm:SubtaskDiffRow">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="2">
<TextBlock Text="{Binding Title}"
FontWeight="SemiBold"
<TextBlock Classes="title" Text="{Binding Title}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding DiffStat}"
Opacity="0.7"
FontFamily="{StaticResource MonoFamily}"
FontSize="11"/>
<TextBlock Classes="meta" Text="{Binding DiffStat}"/>
</StackPanel>
</Border>
</DataTemplate>
@@ -89,14 +60,14 @@
</Border>
<!-- Diff content (right pane) -->
<Grid Grid.Column="1" Background="{StaticResource VoidBrush}">
<Grid Grid.Column="1" Background="{DynamicResource VoidBrush}">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding DisplayedDiff, Mode=OneWay}"
IsReadOnly="True"
AcceptsReturn="True"
FontFamily="Consolas,Menlo,monospace"
FontSize="12"
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeBody}"
Background="Transparent"
BorderThickness="0"
Padding="8"/>
@@ -104,6 +75,7 @@
</Grid>
</Grid>
</Grid>
</Border>
</DockPanel>
</ctl:ModalShell>
</Window>

View File

@@ -1,6 +1,4 @@
using Avalonia.Controls;
using Avalonia.Input;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.ViewModels.Planning;
namespace ClaudeDo.Ui.Views.Planning;
@@ -18,10 +16,4 @@ public partial class PlanningDiffView : Window
if (DataContext is PlanningDiffViewModel vm)
vm.CloseAction = Close;
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
}

View File

@@ -0,0 +1,57 @@
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class AutostartShortcutTests
{
private static string TempDir()
{
var dir = Path.Combine(Path.GetTempPath(), "cdautostart-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}
[Fact]
public void Install_creates_lnk_with_expected_name()
{
var startup = TempDir();
var workerDir = TempDir();
try
{
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
File.WriteAllText(workerExe, "");
AutostartShortcut.Install(startup, workerExe);
Assert.True(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
}
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
}
[Fact]
public void Remove_deletes_existing_lnk()
{
var startup = TempDir();
var workerDir = TempDir();
try
{
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
File.WriteAllText(workerExe, "");
AutostartShortcut.Install(startup, workerExe);
AutostartShortcut.Remove(startup);
Assert.False(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
}
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
}
[Fact]
public void Remove_is_noop_when_missing()
{
var startup = TempDir();
try { AutostartShortcut.Remove(startup); } // must not throw
finally { Directory.Delete(startup, true); }
}
}

View File

@@ -1,30 +0,0 @@
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class ScheduledTaskXmlTests
{
[Fact]
public void Build_EmbedsUserExeAndLogonTrigger()
{
var xml = ScheduledTaskXml.Build(
userId: "MACHINE\\mika",
workerExePath: @"C:\Program Files\ClaudeDo\worker\ClaudeDo.Worker.exe",
restartIntervalMinutes: 1);
Assert.Contains("<LogonTrigger>", xml);
Assert.Contains("<UserId>MACHINE\\mika</UserId>", xml);
Assert.Contains("<LogonType>InteractiveToken</LogonType>", xml);
Assert.Contains("<Hidden>true</Hidden>", xml);
Assert.Contains("<RunLevel>LeastPrivilege</RunLevel>", xml);
Assert.Contains(@"C:\Program Files\ClaudeDo\worker\ClaudeDo.Worker.exe", xml);
Assert.Contains("<Interval>PT1M</Interval>", xml);
}
[Fact]
public void Build_ClampsRestartIntervalToOneMinuteMinimum()
{
var xml = ScheduledTaskXml.Build("M\\u", @"C:\w.exe", restartIntervalMinutes: 0);
Assert.Contains("<Interval>PT1M</Interval>", xml);
}
}

View File

@@ -0,0 +1,25 @@
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class ShortcutFactoryTests
{
[Fact]
public void CreateShortcut_writes_lnk_file()
{
var dir = Path.Combine(Path.GetTempPath(), "cdshortcut-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
try
{
var target = Path.Combine(dir, "fake.exe");
File.WriteAllText(target, "");
var lnk = Path.Combine(dir, "x.lnk");
ShortcutFactory.CreateShortcut(lnk, target, dir, "desc");
Assert.True(File.Exists(lnk));
}
finally { Directory.Delete(dir, recursive: true); }
}
}

View File

@@ -0,0 +1,22 @@
using ClaudeDo.Ui.ViewModels;
using Xunit;
namespace ClaudeDo.Ui.Tests;
public class ConnectionPromptGateTests
{
[Fact]
public void Shows_once_when_offline()
{
var vm = new IslandsShellViewModel();
Assert.True(vm.DecideShowConnectionPrompt(isOffline: true));
Assert.False(vm.DecideShowConnectionPrompt(isOffline: true)); // not a second time
}
[Fact]
public void Does_not_show_when_connected_before_grace()
{
var vm = new IslandsShellViewModel();
Assert.False(vm.DecideShowConnectionPrompt(isOffline: false));
}
}