108 Commits

Author SHA1 Message Date
Mika Kuns
4a36fbe5e0 feat(ui): replay run log in session terminal, drop per-row live tail
All checks were successful
Release / release (push) Successful in 34s
Set the task's log path when the run is created (not at completion) so the
session terminal can replay live output when the user navigates away and back
mid-run. Remove the now-redundant inline per-row live tail (LiveTail /
HasLiveTail / TaskMessageEvent) and scroll the terminal to end after the next
layout pass so wrapping lines aren't clipped.
2026-06-01 16:25:14 +02:00
Mika Kuns
9e5a3fe962 merge: MCP surface — worktree/diff/merge/log tools + status-enum docs 2026-06-01 16:21:51 +02:00
Mika Kuns
3f98fd0ae5 merge: normalize list ID format to dashed UUID 2026-06-01 16:21:50 +02:00
Mika Kuns
8420b87bd1 merge: run reporting — token accounting + populate empty result 2026-06-01 16:21:50 +02:00
Mika Kuns
c0978df19a feat(claude-do): MCP surface: worktree/diff/merge/log tools + status-enum doc
BUNDLE — all changes live in src/ClaudeDo.Worker/External/ExternalMcpService.cs only, so this is one worktree / one merge. Do NOT touch run-recording or data-layer code (those are separate tasks). Reuse the existing services behind the UI modals (WorktreesOverviewModalView, DiffModalView, MergeModalView) — do not reimplement git plumbing. Build green after each addition.

Add these MCP tools:
1. g

ClaudeDo-Task: f6bdfb5b-8cbf-4e65-93d4-6c758a160484
2026-06-01 16:15:26 +02:00
Mika Kuns
3ac9e030e2 chore(claude-do): Normalize list ID format
list_task_lists returns two different ID formats: dashed UUIDs (e.g. "caed660e-109f-4e2a-b055-2c2722bf6fb7") and compact 32-char hex (e.g. "5c2cafcb33f044069ac324ac3fd84a16"). Mixing formats makes equality checks, logging, and lookups error-prone.

Fix: pick one canonical format (recommend dashed UUID) and normalize on write + migrate existing records. Ensure all ID-returning tools emit the same f

ClaudeDo-Task: fa8b69e0-6f8d-41d7-9a41-88db1360544d
2026-06-01 16:06:59 +02:00
Mika Kuns
4c6e6594dc fix(claude-do): Run reporting: token accounting + populate empty result
BUNDLE — both fixes live in the Worker run-recording / persistence layer (where a TaskRun is written after an agent finishes), NOT in ExternalMcpService.cs. Keep this disjoint from the MCP-surface bundle so the two can run in parallel without worktree conflicts. The DTO fields (tokensIn, tokensOut, resultMarkdown) already exist and are surfaced by list_runs/get_run — the bug is at write time.

1.

ClaudeDo-Task: 49a6060a-5044-4f1b-8665-5cfc064b8a82
2026-06-01 16:01:11 +02:00
mika kuns
5170914a7a feat(installer): optionally register ClaudeDo MCP server with Claude
Add an install step and welcome-page opt-in that registers the ClaudeDo
external MCP server with the Claude CLI. Failures are non-fatal and surface
the manual command so a missing or old CLI never blocks the install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 15:51:44 +02:00
mika kuns
b1f4349dab feat(worker): configurable max parallel task executions
Add a "Max parallel executions" setting to the General settings tab so
the queue can run more than one task concurrently. QueueService now
tracks multiple active slots and reads the limit from app settings each
cycle, so changes take effect without restarting the worker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 15:51:12 +02:00
Mika Kuns
23326a1833 merge: return confirmation payload from delete_task and cancel_task 2026-06-01 15:29:30 +02:00
Mika Kuns
ca0594328a merge: make add_task optional params actually optional 2026-06-01 15:29:29 +02:00
Mika Kuns
22d06acb35 merge: fix inconsistent timezone on timestamps (Z suffix) 2026-06-01 15:29:16 +02:00
Mika Kuns
ab44ba5e41 feat(ui): list reordering, quick actions, and resizable modals
- Drag-to-reorder user lists in the sidebar, persisted via a new
  list sort_order column (AddListSortOrder migration, backfilled by
  creation time) and ListRepository.ReorderAsync
- "Open in Explorer" / "Open in Terminal" context-menu actions on lists
- "Clear all completed" button on the Tasks island
- Inline-edit subtask titles (empty text deletes the step) and
  click-to-copy task ID in the Details island
- Make modal and planning windows resizable (BorderOnly decorations
  with min sizes) instead of fixed-size borderless

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:28:17 +02:00
mika kuns
6c3afce329 chore(claude-do): Return confirmation payload from delete_task and cancel_task
delete_task (and likely cancel_task) return no output on success. Silent success is indistinguishable from a no-op, so callers can't verify the action took effect.

Fix: return a small confirmation object, e.g. { deleted: true, id } / { cancelled: true, id }. Indicate not-found vs deleted distinctly.

ClaudeDo-Task: 97a87ebb-0d87-4ee0-800c-aa1a0b3a06c5
2026-06-01 15:20:20 +02:00
mika kuns
f8e387bbc1 chore(claude-do): Make add_task optional params actually optional
add_task currently marks description, createdBy, and queueImmediately as required, forcing callers to invent values for fields that have obvious defaults.

Fix: make them optional with sensible defaults — description: null, queueImmediately: false, createdBy: server default like "mcp". Keep only listId and title as truly required.

ClaudeDo-Task: b9fadf0b-a20e-4deb-932d-29ef9c0b83f3
2026-06-01 15:18:27 +02:00
mika kuns
2a36998ac7 chore(claude-do): Fix inconsistent timezone on timestamps
Timestamps are serialized inconsistently across tools. add_task returns createdAt with a trailing 'Z' (e.g. "2026-06-01T13:03:56.1636946Z"), but get_task and list_runs return the same value WITHOUT the 'Z'. This is a timezone-ambiguity bug.

Fix: serialize all DateTime values as UTC with the 'Z' suffix consistently (use a single shared JSON serializer setting / DateTimeKind=Utc). Audit every tool

ClaudeDo-Task: 4bbc759e-ff05-45e3-a57f-b290c7e16264
2026-06-01 15:16:25 +02:00
mika kuns
4148dcdb18 fix(installer): stop the running app before updating, not just the worker
All checks were successful
Release / release (push) Successful in 34s
A running ClaudeDo.App.exe locks the install\app directory, so the extract
step's Directory.Move failed with "Access to the path '...\app' is denied"
during an update. StopWorkerStep now also terminates app processes scoped to
the install dir (benefits uninstall too).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 13:26:47 +02:00
mika kuns
5783790733 fix(installer): keep step badges green and reset state on re-run
Step status and output lines arrive on two separate Progress<T> channels, so a
trailing "Running" line-message could be delivered after a step's terminal
Done/Failed and downgrade the badge back to orange. Guard against that
downgrade. Also reset each step's messages/status/expansion at the start of a
run so re-running no longer appends to the previous run's output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 13:22:36 +02:00
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
109 changed files with 11861 additions and 1616 deletions

2
.gitignore vendored
View File

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

View File

@@ -161,11 +161,13 @@ Voraussetzung: funktionierendes Gitea-Release unter `git.kuns.dev/releases/Claud
## 4. Service-Deployment ## 4. Service-Deployment
### 4.1 Worker-Autostart als Per-User-Task ✅ (ersetzt Windows-Service) ### 4.1 Worker-Autostart via Startup-Shortcut ✅ (ersetzt Scheduled Task + 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. - Der Worker läuft als `WinExe` (kein Konsolenfenster) + Serilog-File-Sink (`~/.todo-app/logs/worker-*.log`) + Single-Instance-Mutex.
- Worker ist `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.
- 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. - `StartWorkerStep` startet den Worker per `Process.Start`; `StopWorkerStep` beendet ihn per prozessbasiertem Kill.
- 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). - 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 ✅ ### 4.2 Pfad-Auflösung absolut ✅
- `WorkerConfig.Load` expandiert `~`/`%USERPROFILE%` für alle Pfad-Felder. - `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. 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:** **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.
- 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.
**Install:** **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.
- 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.
**Auth-Konflikt mit "User-CLI-Session" beachten:** **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).
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:
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". **Logging:** Serilog-File-Sink nach `~/.todo-app/logs/worker-*.log`. Single-Instance-Mutex verhindert parallele Instanzen.
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".
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) ## Project-Layout (Monorepo)
@@ -319,4 +304,4 @@ Vorteil Monorepo: gemeinsames `schema.sql`, atomische Änderungen über UI+Worke
- Bulk-Discard alter Worktrees. - Bulk-Discard alter Worktrees.
- Anzeige der ndjson-Message-Chronik im UI. - Anzeige der ndjson-Message-Chronik im UI.
- Windows Job Objects für garantierten Child-Cleanup beim Worker-Crash. - 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>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ClaudeDo.Ui/Design/Tokens.axaml" /> <ResourceInclude Source="avares://ClaudeDo.Ui/Design/Tokens.axaml" />
<ResourceInclude Source="avares://ClaudeDo.Ui/Views/Controls/ModalShell.axaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- Converters --> <!-- Converters -->
@@ -31,6 +32,13 @@
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" /> <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"> <Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentGlowBrush}"/> <Setter Property="Background" Value="{DynamicResource AccentGlowBrush}"/>
</Style> </Style>

View File

@@ -3,6 +3,7 @@ using ClaudeDo.Data.Seeding;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ClaudeDo.Data; namespace ClaudeDo.Data;
@@ -19,9 +20,24 @@ public class ClaudeDoDbContext : DbContext
public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>(); public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>();
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>(); public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
private static readonly ValueConverter<DateTime?, DateTime?> UtcNullableConverter =
new(v => v, v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : null);
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly); modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime) && property.GetValueConverter() == null)
property.SetValueConverter(UtcConverter);
else if (property.ClrType == typeof(DateTime?) && property.GetValueConverter() == null)
property.SetValueConverter(UtcNullableConverter);
}
} }
/// <summary> /// <summary>

View File

@@ -22,6 +22,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
builder.Property(s => s.DefaultPermissionMode) builder.Property(s => s.DefaultPermissionMode)
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions"); .HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
builder.Property(s => s.MaxParallelExecutions)
.HasColumnName("max_parallel_executions").IsRequired().HasDefaultValue(1);
builder.Property(s => s.WorktreeStrategy) builder.Property(s => s.WorktreeStrategy)
.HasColumnName("worktree_strategy").IsRequired().HasDefaultValue("sibling"); .HasColumnName("worktree_strategy").IsRequired().HasDefaultValue("sibling");
builder.Property(s => s.CentralWorktreeRoot) builder.Property(s => s.CentralWorktreeRoot)

View File

@@ -16,6 +16,9 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired(); builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired();
builder.Property(l => l.WorkingDir).HasColumnName("working_dir"); builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore"); builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
builder.HasOne(l => l.Config) builder.HasOne(l => l.Config)
.WithOne(c => c.List) .WithOne(c => c.List)

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,600 @@
// <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("20260601114247_AddListSortOrder")]
partial class AddListSortOrder
{
/// <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<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
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,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListSortOrder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "sort_order",
table: "lists",
type: "INTEGER",
nullable: false,
defaultValue: 0);
// Backfill existing rows with a dense order (0..N-1) by creation time
// so today's sidebar order is preserved after the migration.
migrationBuilder.Sql("""
WITH ordered AS (
SELECT id, (row_number() OVER (ORDER BY created_at) - 1) AS rn
FROM lists
)
UPDATE lists SET sort_order = (SELECT rn FROM ordered WHERE ordered.id = lists.id);
""");
migrationBuilder.CreateIndex(
name: "idx_lists_sort",
table: "lists",
column: "sort_order");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "idx_lists_sort",
table: "lists");
migrationBuilder.DropColumn(
name: "sort_order",
table: "lists");
}
}
}

View File

@@ -0,0 +1,607 @@
// <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("20260601133737_AddMaxParallelExecutions")]
partial class AddMaxParallelExecutions
{
/// <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>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
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",
MaxParallelExecutions = 1,
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<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
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,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddMaxParallelExecutions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "max_parallel_executions",
table: "app_settings",
type: "INTEGER",
nullable: false,
defaultValue: 1);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
column: "max_parallel_executions",
value: 1);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "max_parallel_executions",
table: "app_settings");
}
}
}

View File

@@ -0,0 +1,607 @@
// <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("20260601140000_NormalizeListIdFormat")]
partial class NormalizeListIdFormat
{
/// <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>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
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",
MaxParallelExecutions = 1,
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<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
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,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class NormalizeListIdFormat : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// SQLite: PRAGMA foreign_keys must run outside a transaction.
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;", suppressTransaction: true);
// Normalize tasks.list_id: 32-char compact hex → 36-char dashed UUID
migrationBuilder.Sql("""
UPDATE tasks
SET list_id = substr(list_id,1,8)||'-'||substr(list_id,9,4)||'-'||substr(list_id,13,4)||'-'||substr(list_id,17,4)||'-'||substr(list_id,21,12)
WHERE length(list_id) = 32;
""");
// Normalize list_config.list_id (also the PK of that table)
migrationBuilder.Sql("""
UPDATE list_config
SET list_id = substr(list_id,1,8)||'-'||substr(list_id,9,4)||'-'||substr(list_id,13,4)||'-'||substr(list_id,17,4)||'-'||substr(list_id,21,12)
WHERE length(list_id) = 32;
""");
// Normalize lists.id (PK — must come last)
migrationBuilder.Sql("""
UPDATE lists
SET id = substr(id,1,8)||'-'||substr(id,9,4)||'-'||substr(id,13,4)||'-'||substr(id,17,4)||'-'||substr(id,21,12)
WHERE length(id) = 32;
""");
migrationBuilder.Sql("PRAGMA foreign_keys = ON;", suppressTransaction: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;", suppressTransaction: true);
migrationBuilder.Sql("UPDATE tasks SET list_id = replace(list_id,'-','') WHERE length(list_id) = 36;");
migrationBuilder.Sql("UPDATE list_config SET list_id = replace(list_id,'-','') WHERE length(list_id) = 36;");
migrationBuilder.Sql("UPDATE lists SET id = replace(id,'-','') WHERE length(id) = 36;");
migrationBuilder.Sql("PRAGMA foreign_keys = ON;", suppressTransaction: true);
}
}
}

View File

@@ -54,6 +54,12 @@ namespace ClaudeDo.Data.Migrations
.HasDefaultValue("bypassPermissions") .HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode"); .HasColumnName("default_permission_mode");
b.Property<int>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
b.Property<string>("RepoImportFolders") b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("repo_import_folders"); .HasColumnName("repo_import_folders");
@@ -89,6 +95,7 @@ namespace ClaudeDo.Data.Migrations
DefaultMaxTurns = 100, DefaultMaxTurns = 100,
DefaultModel = "sonnet", DefaultModel = "sonnet",
DefaultPermissionMode = "auto", DefaultPermissionMode = "auto",
MaxParallelExecutions = 1,
WorktreeAutoCleanupDays = 7, WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false, WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling" WorktreeStrategy = "sibling"
@@ -140,12 +147,21 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("name"); .HasColumnName("name");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir") b.Property<string>("WorkingDir")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("working_dir"); .HasColumnName("working_dir");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
b.ToTable("lists", (string)null); b.ToTable("lists", (string)null);
}); });

View File

@@ -11,6 +11,8 @@ public sealed class AppSettingsEntity
public int DefaultMaxTurns { get; set; } = 100; public int DefaultMaxTurns { get; set; } = 100;
public string DefaultPermissionMode { get; set; } = "auto"; public string DefaultPermissionMode { get; set; } = "auto";
public int MaxParallelExecutions { get; set; } = 1;
public string WorktreeStrategy { get; set; } = "sibling"; public string WorktreeStrategy { get; set; } = "sibling";
public string? CentralWorktreeRoot { get; set; } public string? CentralWorktreeRoot { get; set; }
public bool WorktreeAutoCleanupEnabled { get; set; } public bool WorktreeAutoCleanupEnabled { get; set; }

View File

@@ -7,6 +7,7 @@ public sealed class ListEntity
public required DateTime CreatedAt { get; init; } public required DateTime CreatedAt { get; init; }
public string? WorkingDir { get; set; } public string? WorkingDir { get; set; }
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType; public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
public int SortOrder { get; set; }
// Navigation properties // Navigation properties
public ListConfigEntity? Config { get; set; } public ListConfigEntity? Config { get; set; }

View File

@@ -44,6 +44,7 @@ public sealed class AppSettingsRepository
row.DefaultMaxTurns = updated.DefaultMaxTurns; row.DefaultMaxTurns = updated.DefaultMaxTurns;
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode) row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
? "auto" : updated.DefaultPermissionMode; ? "auto" : updated.DefaultPermissionMode;
row.MaxParallelExecutions = updated.MaxParallelExecutions < 1 ? 1 : updated.MaxParallelExecutions;
row.WorktreeStrategy = string.IsNullOrWhiteSpace(updated.WorktreeStrategy) ? "sibling" : updated.WorktreeStrategy; row.WorktreeStrategy = string.IsNullOrWhiteSpace(updated.WorktreeStrategy) ? "sibling" : updated.WorktreeStrategy;
row.CentralWorktreeRoot = string.IsNullOrWhiteSpace(updated.CentralWorktreeRoot) row.CentralWorktreeRoot = string.IsNullOrWhiteSpace(updated.CentralWorktreeRoot)
? null : updated.CentralWorktreeRoot; ? null : updated.CentralWorktreeRoot;

View File

@@ -33,7 +33,19 @@ public sealed class ListRepository
public async Task<List<ListEntity>> GetAllAsync(CancellationToken ct = default) public async Task<List<ListEntity>> GetAllAsync(CancellationToken ct = default)
{ {
return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct); return await _context.Lists.OrderBy(l => l.SortOrder).ThenBy(l => l.CreatedAt).ToListAsync(ct);
}
public async Task ReorderAsync(IReadOnlyList<string> orderedListIds, CancellationToken ct = default)
{
var idSet = orderedListIds.ToHashSet();
var entities = await _context.Lists.Where(l => idSet.Contains(l.Id)).ToListAsync(ct);
for (int i = 0; i < orderedListIds.Count; i++)
{
var e = entities.FirstOrDefault(x => x.Id == orderedListIds[i]);
if (e is not null) e.SortOrder = i;
}
await _context.SaveChangesAsync(ct);
} }
public async Task<ListConfigEntity?> GetConfigAsync(string listId, CancellationToken ct = default) public async Task<ListConfigEntity?> GetConfigAsync(string listId, CancellationToken ct = default)

View File

@@ -15,7 +15,7 @@ public static class DefaultListsSeeder
{ {
ctx.Lists.Add(new ListEntity ctx.Lists.Add(new ListEntity
{ {
Id = Guid.NewGuid().ToString("N"), Id = Guid.NewGuid().ToString(),
Name = name, Name = name,
CreatedAt = now, CreatedAt = now,
}); });

View File

@@ -209,6 +209,8 @@ public partial class App : Application
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>()); sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>());
sc.AddSingleton<IInstallStep, WriteConfigStep>(); sc.AddSingleton<IInstallStep, WriteConfigStep>();
sc.AddSingleton<IInstallStep, InitDatabaseStep>(); sc.AddSingleton<IInstallStep, InitDatabaseStep>();
sc.AddSingleton<RegisterMcpStep>();
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<RegisterMcpStep>());
sc.AddSingleton<RegisterAutostartStep>(); sc.AddSingleton<RegisterAutostartStep>();
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<RegisterAutostartStep>()); sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<RegisterAutostartStep>());
sc.AddSingleton<IInstallStep, CreateShortcutsStep>(); sc.AddSingleton<IInstallStep, CreateShortcutsStep>();

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

@@ -32,4 +32,8 @@ public sealed class InstallContext
// InstallPage // InstallPage
public bool CreateDesktopShortcut { get; set; } = true; public bool CreateDesktopShortcut { get; set; } = true;
// WelcomePage — register the external MCP endpoint with the Claude CLI.
public bool RegisterMcpWithClaude { get; set; } = true;
public int ExternalMcpPort { get; set; } = 47_822;
} }

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

View File

@@ -84,6 +84,15 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
{ {
if (IsInstalling) return; if (IsInstalling) return;
// Reset per-step state so a re-run starts clean instead of appending
// output to the previous run's messages.
foreach (var s in Steps)
{
s.Messages.Clear();
s.Status = StepStatus.Pending;
s.IsExpanded = false;
}
IsInstalling = true; IsInstalling = true;
IsComplete = false; IsComplete = false;
HasErrors = false; HasErrors = false;
@@ -96,6 +105,10 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
var step = Steps.FirstOrDefault(s => s.Name == p.StepName); var step = Steps.FirstOrDefault(s => s.Name == p.StepName);
if (step is null) return; if (step is null) return;
// Status and output lines arrive on two separate Progress<T> channels, so a
// trailing "Running" line-message can be delivered after the step's terminal
// Done/Failed. Never let that downgrade a completed step back to Running.
if (!(step.Status is StepStatus.Done or StepStatus.Failed && p.Status is StepStatus.Running))
step.Status = p.Status; step.Status = p.Status;
if (p.Message is not null) if (p.Message is not null)
{ {
@@ -135,6 +148,7 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
_serviceProvider.GetRequiredService<DownloadAndExtractStep>(), _serviceProvider.GetRequiredService<DownloadAndExtractStep>(),
// Migrates the legacy service away and (re)registers the logon task. // Migrates the legacy service away and (re)registers the logon task.
_serviceProvider.GetRequiredService<RegisterAutostartStep>(), _serviceProvider.GetRequiredService<RegisterAutostartStep>(),
_serviceProvider.GetRequiredService<RegisterMcpStep>(),
_serviceProvider.GetRequiredService<StartWorkerStep>(), _serviceProvider.GetRequiredService<StartWorkerStep>(),
_serviceProvider.GetRequiredService<WriteInstallManifestStep>(), _serviceProvider.GetRequiredService<WriteInstallManifestStep>(),
// Refresh the bundled uninstaller exe + Add/Remove-Programs version so a // Refresh the bundled uninstaller exe + Add/Remove-Programs version so a

View File

@@ -32,6 +32,14 @@
<TextBlock Text="{Binding InstallError}" Foreground="{StaticResource ErrorBrush}" FontSize="11" <TextBlock Text="{Binding InstallError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
Visibility="{Binding InstallError, Converter={StaticResource NullToCollapsedConverter}}"/> Visibility="{Binding InstallError, Converter={StaticResource NullToCollapsedConverter}}"/>
<CheckBox Content="Register MCP server with Claude"
IsChecked="{Binding RegisterMcp}"
Margin="0,24,0,0"/>
<TextBlock Text="Runs 'claude mcp add' so Claude can view and manage your ClaudeDo tasks. You can change this later."
TextWrapping="Wrap" FontSize="11"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,4,0,0"/>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -24,6 +24,7 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
[ObservableProperty] private string _heading = "Install ClaudeDo"; [ObservableProperty] private string _heading = "Install ClaudeDo";
[ObservableProperty] private string _subheading = "Set the installation directory and continue."; [ObservableProperty] private string _subheading = "Set the installation directory and continue.";
[ObservableProperty] private bool _installDirEditable = true; [ObservableProperty] private bool _installDirEditable = true;
[ObservableProperty] private bool _registerMcp = true;
public WelcomePageViewModel(InstallContext context) public WelcomePageViewModel(InstallContext context)
{ {
@@ -62,6 +63,7 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
public Task ApplyAsync() public Task ApplyAsync()
{ {
_context.InstallDirectory = InstallDirectory; _context.InstallDirectory = InstallDirectory;
_context.RegisterMcpWithClaude = RegisterMcp;
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,7 +1,4 @@
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps; namespace ClaudeDo.Installer.Steps;
@@ -23,7 +20,7 @@ public sealed class CreateShortcutsStep : IInstallStep
"Programs"); "Programs");
Directory.CreateDirectory(startMenuDir); Directory.CreateDirectory(startMenuDir);
var startMenuPath = Path.Combine(startMenuDir, "ClaudeDo.lnk"); 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}"); progress.Report($"Created Start Menu shortcut: {startMenuPath}");
// Desktop shortcut (optional) // Desktop shortcut (optional)
@@ -32,7 +29,7 @@ public sealed class CreateShortcutsStep : IInstallStep
var desktopPath = Path.Combine( var desktopPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
"ClaudeDo.lnk"); "ClaudeDo.lnk");
CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager"); ShortcutFactory.CreateShortcut(desktopPath, appExe, workingDir, "ClaudeDo Task Manager");
progress.Report($"Created Desktop shortcut: {desktopPath}"); 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.IO;
using System.Security.Principal;
using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps; namespace ClaudeDo.Installer.Steps;
public sealed class RegisterAutostartStep : IInstallStep public sealed class RegisterAutostartStep : IInstallStep
{ {
public const string TaskName = "ClaudeDoWorker"; public const string LegacyTaskName = "ClaudeDoWorker";
private const string LegacyServiceName = "ClaudeDoWorker"; private const string LegacyServiceName = "ClaudeDoWorker";
public string Name => "Register Autostart"; public string Name => "Register Autostart";
@@ -34,24 +33,19 @@ public sealed class RegisterAutostartStep : IInstallStep
} }
} }
// 2) Register (or replace) the per-user logon task. // 2) Migrate away the legacy logon scheduled task if present (best-effort).
var userId = WindowsIdentity.GetCurrent().Name; progress.Report("Removing legacy logon task...");
var minutes = Math.Max(1, ctx.RestartDelayMs / 60000); await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
var xml = ScheduledTaskXml.Build(userId, workerExe, minutes);
var xmlPath = Path.Combine(Path.GetTempPath(), $"ClaudeDoWorker-{Guid.NewGuid():N}.xml"); // 3) Register per-user autostart via a Startup-folder shortcut.
await File.WriteAllTextAsync(xmlPath, xml, new System.Text.UnicodeEncoding(false, true), ct); progress.Report("Creating Startup shortcut...");
try try
{ {
progress.Report("Registering logon task..."); AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
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}");
} }
finally catch (Exception ex)
{ {
try { File.Delete(xmlPath); } catch { /* best effort */ } return StepResult.Fail($"Failed to create Startup shortcut: {ex.Message}");
} }
return StepResult.Ok(); return StepResult.Ok();

View File

@@ -0,0 +1,47 @@
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class RegisterMcpStep : IInstallStep
{
private const string ServerName = "claudedo";
public string Name => "Register MCP with Claude";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
if (!ctx.RegisterMcpWithClaude)
{
progress.Report("Skipped (not selected)");
return StepResult.Ok();
}
var url = $"http://127.0.0.1:{ctx.ExternalMcpPort}/mcp";
// Drop any prior registration first so a re-run (e.g. update, changed port)
// overwrites cleanly instead of erroring on a duplicate name.
progress.Report($"Removing existing '{ServerName}' MCP registration (if any)...");
await ProcessRunner.RunAsync(ctx.ClaudeBin, $"mcp remove --scope user {ServerName}", null, progress, ct);
progress.Report($"Registering '{ServerName}' MCP server at {url}...");
var (exit, output) = await ProcessRunner.RunAsync(
ctx.ClaudeBin,
$"mcp add --transport http --scope user {ServerName} {url}",
null, progress, ct);
// Non-fatal: a missing/old Claude CLI must never block the install. Surface the
// manual command so the user can register it themselves later.
if (exit != 0)
{
progress.Report(
$"Could not register MCP automatically (claude exited {exit}). " +
$"Run manually: claude mcp add --transport http --scope user {ServerName} {url}");
}
else
{
progress.Report("MCP server registered with Claude.");
}
return StepResult.Ok();
}
}

View File

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

View File

@@ -6,19 +6,22 @@ namespace ClaudeDo.Installer.Steps;
public sealed class StopWorkerStep : IInstallStep public sealed class StopWorkerStep : IInstallStep
{ {
public const string TaskName = "ClaudeDoWorker"; public const string LegacyTaskName = "ClaudeDoWorker";
public const string ProcessName = "ClaudeDo.Worker";
// Both must be stopped before the install dir is touched: a running app/worker
// exe locks its directory, so Directory.Move during extraction would otherwise
// fail with "Access to the path '...\app' is denied".
private static readonly string[] ProcessNames = { "ClaudeDo.Worker", "ClaudeDo.App" };
public string Name => "Stop Worker"; public string Name => "Stop Worker";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct) public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{ {
progress.Report("Stopping worker task (if running)..."); progress.Report("Stopping ClaudeDo processes (if running)...");
await ProcessRunner.RunAsync("schtasks.exe", $"/End /TN \"{TaskName}\"", null, progress, ct);
progress.Report("Stopping worker process (if running)...");
var installDir = ctx.InstallDirectory; var installDir = ctx.InstallDirectory;
foreach (var p in Process.GetProcessesByName(ProcessName)) foreach (var name in ProcessNames)
{
foreach (var p in Process.GetProcessesByName(name))
{ {
try try
{ {
@@ -30,6 +33,7 @@ public sealed class StopWorkerStep : IInstallStep
catch { /* process may have exited or be inaccessible */ } catch { /* process may have exited or be inaccessible */ }
finally { p.Dispose(); } finally { p.Dispose(); }
} }
}
await Task.CompletedTask; await Task.CompletedTask;
return StepResult.Ok(); return StepResult.Ok();
} }

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> <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 --> <!-- Badge brushes -->
<SolidColorBrush x:Key="DraftBadgeBrush" Color="#4A5568"/> <SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="#D69E2E"/> <SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="#3182CE"/> <SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
</Styles.Resources> </Styles.Resources>
@@ -120,7 +120,7 @@
<!-- ============================================================ --> <!-- ============================================================ -->
<Style Selector="Border.island"> <Style Selector="Border.island">
<Setter Property="Background" Value="{StaticResource IslandBackgroundBrush}" /> <Setter Property="Background" Value="{StaticResource IslandBackgroundBrush}" />
<Setter Property="BorderBrush" Value="#0DFFFFFF" /> <Setter Property="BorderBrush" Value="{StaticResource HairlineOverlayBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource IslandCornerRadius}" /> <Setter Property="CornerRadius" Value="{StaticResource IslandCornerRadius}" />
<Setter Property="BoxShadow" Value="{StaticResource IslandShadow}" /> <Setter Property="BoxShadow" Value="{StaticResource IslandShadow}" />
@@ -146,30 +146,30 @@
</Style> </Style>
<Style Selector="Border.chip > TextBlock"> <Style Selector="Border.chip > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" /> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style> </Style>
<!-- Status variants — tint background 12% alpha of the status hue --> <!-- Status variants — tint background 12% alpha of the status hue -->
<Style Selector="Border.chip.running"> <Style Selector="Border.chip.running">
<Setter Property="Background" Value="#1F7C9166" /> <Setter Property="Background" Value="{StaticResource RunningTintBrush}" />
<Setter Property="BorderBrush" Value="#4C7C9166" /> <Setter Property="BorderBrush" Value="{StaticResource RunningTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.chip.running > TextBlock"> <Style Selector="Border.chip.running > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusRunningBrush}" /> <Setter Property="Foreground" Value="{StaticResource StatusRunningBrush}" />
</Style> </Style>
<Style Selector="Border.chip.review"> <Style Selector="Border.chip.review">
<Setter Property="Background" Value="#1FD4A574" /> <Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
<Setter Property="BorderBrush" Value="#4CD4A574" /> <Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.chip.review > TextBlock"> <Style Selector="Border.chip.review > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" /> <Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" />
</Style> </Style>
<Style Selector="Border.chip.error"> <Style Selector="Border.chip.error">
<Setter Property="Background" Value="#1FC87060" /> <Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
<Setter Property="BorderBrush" Value="#4CC87060" /> <Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.chip.error > TextBlock"> <Style Selector="Border.chip.error > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusErrorBrush}" /> <Setter Property="Foreground" Value="{StaticResource StatusErrorBrush}" />
@@ -177,8 +177,8 @@
<!-- queued → Sage (#8B9D7A) --> <!-- queued → Sage (#8B9D7A) -->
<Style Selector="Border.chip.queued"> <Style Selector="Border.chip.queued">
<Setter Property="Background" Value="#1F8B9D7A" /> <Setter Property="Background" Value="{StaticResource QueuedTintBrush}" />
<Setter Property="BorderBrush" Value="#4C8B9D7A" /> <Setter Property="BorderBrush" Value="{StaticResource QueuedTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.chip.queued > TextBlock"> <Style Selector="Border.chip.queued > TextBlock">
<Setter Property="Foreground" Value="{StaticResource StatusQueuedBrush}" /> <Setter Property="Foreground" Value="{StaticResource StatusQueuedBrush}" />
@@ -203,7 +203,7 @@
<Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" /> <Setter Property="CornerRadius" Value="{StaticResource ButtonCornerRadius}" />
<Setter Property="Padding" Value="10,6" /> <Setter Property="Padding" Value="10,6" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
@@ -216,11 +216,6 @@
<Setter Property="Background" Value="{StaticResource Surface3Brush}" /> <Setter Property="Background" Value="{StaticResource Surface3Brush}" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
</Style> </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 --> <!-- Icon button: 24×24 square with hover surface -->
<Style Selector="Button.icon-btn"> <Style Selector="Button.icon-btn">
@@ -248,7 +243,7 @@
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource InputCornerRadius}" /> <Setter Property="CornerRadius" Value="{StaticResource InputCornerRadius}" />
<Setter Property="Padding" Value="10,8" /> <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="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" /> <Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
</Style> </Style>
@@ -310,8 +305,9 @@
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<BrushTransition Property="Background" Duration="0:0:0.10"/> <BrushTransition Property="Background" Duration="0:0:0.12" />
<BrushTransition Property="BorderBrush" Duration="0:0:0.10"/> <BrushTransition Property="BorderBrush" Duration="0:0:0.12" />
<ThicknessTransition Property="Margin" Duration="0:0:0.15" />
</Transitions> </Transitions>
</Setter> </Setter>
</Style> </Style>
@@ -358,22 +354,22 @@
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
</Style> </Style>
<Style Selector="Border.agent-strip.running"> <Style Selector="Border.agent-strip.running">
<Setter Property="Background" Value="#147C9166" /> <Setter Property="Background" Value="{StaticResource RunningTintBrush}" />
<Setter Property="BorderBrush" Value="#4C7C9166" /> <Setter Property="BorderBrush" Value="{StaticResource RunningTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.agent-strip.review"> <Style Selector="Border.agent-strip.review">
<Setter Property="Background" Value="#14D4A574" /> <Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
<Setter Property="BorderBrush" Value="#4CD4A574" /> <Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
</Style> </Style>
<Style Selector="Border.agent-strip.error"> <Style Selector="Border.agent-strip.error">
<Setter Property="Background" Value="#14C87060" /> <Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
<Setter Property="BorderBrush" Value="#4CC87060" /> <Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
</Style> </Style>
<!-- queued → Sage tint --> <!-- queued → Sage tint -->
<Style Selector="Border.agent-strip.queued"> <Style Selector="Border.agent-strip.queued">
<Setter Property="Background" Value="#148B9D7A" /> <Setter Property="Background" Value="{StaticResource QueuedTintBrush}" />
<Setter Property="BorderBrush" Value="#4C8B9D7A" /> <Setter Property="BorderBrush" Value="{StaticResource QueuedTintBorderBrush}" />
</Style> </Style>
<!-- idle → neutral (same as base, explicit for clarity) --> <!-- idle → neutral (same as base, explicit for clarity) -->
@@ -386,7 +382,7 @@
<!-- TERMINAL / LOG --> <!-- TERMINAL / LOG -->
<!-- ============================================================ --> <!-- ============================================================ -->
<Style Selector="Border.terminal"> <Style Selector="Border.terminal">
<Setter Property="Background" Value="#FF080C0B" /> <Setter Property="Background" Value="{StaticResource VoidBrush}" />
<Setter Property="CornerRadius" Value="8" /> <Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="12" /> <Setter Property="Padding" Value="12" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
@@ -394,7 +390,7 @@
</Style> </Style>
<Style Selector="Border.terminal TextBlock"> <Style Selector="Border.terminal TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextDimBrush}" />
</Style> </Style>
<Style Selector="Border.terminal TextBlock[Tag=log-sys]"> <Style Selector="Border.terminal TextBlock[Tag=log-sys]">
@@ -449,7 +445,7 @@
</Style> </Style>
<Style Selector="Border.live-chip > StackPanel > TextBlock"> <Style Selector="Border.live-chip > StackPanel > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource AccentBrush}" />
<Setter Property="LetterSpacing" Value="1.2" /> <Setter Property="LetterSpacing" Value="1.2" />
</Style> </Style>
@@ -471,7 +467,7 @@
<!-- Terminal log-line timestamp column --> <!-- Terminal log-line timestamp column -->
<Style Selector="TextBlock.log-ts"> <Style Selector="TextBlock.log-ts">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="Width" Value="60" /> <Setter Property="Width" Value="60" />
<Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="VerticalAlignment" Value="Top" />
@@ -480,7 +476,7 @@
<!-- Kind marker column --> <!-- Kind marker column -->
<Style Selector="TextBlock.log-kind"> <Style Selector="TextBlock.log-kind">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="Width" Value="46" /> <Setter Property="Width" Value="46" />
<Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="VerticalAlignment" Value="Top" />
@@ -554,7 +550,7 @@
<!-- Count badge — larger, high contrast, brighter when the row is active --> <!-- Count badge — larger, high contrast, brighter when the row is active -->
<Style Selector="TextBlock.list-count"> <Style Selector="TextBlock.list-count">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="FontWeight" Value="Medium" /> <Setter Property="FontWeight" Value="Medium" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
@@ -565,17 +561,6 @@
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
</Style> </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 --> <!-- SEARCH BOX WRAPPER -->
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -594,7 +579,7 @@
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="4,7" /> <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="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" /> <Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
</Style> </Style>
@@ -618,7 +603,7 @@
</Style> </Style>
<Style Selector="Border.kbd > TextBlock"> <Style Selector="Border.kbd > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
@@ -633,7 +618,7 @@
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,8" /> <Setter Property="Padding" Value="10,8" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
@@ -694,7 +679,7 @@
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" 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="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" /> <Setter Property="CaretBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="MinHeight" Value="20" /> <Setter Property="MinHeight" Value="20" />
@@ -722,22 +707,6 @@
<!-- TASK ROW — extensions (C2) --> <!-- 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 --> <!-- Left accent bar for selected row -->
<Style Selector="Border.task-row-accent"> <Style Selector="Border.task-row-accent">
<Setter Property="Width" Value="2" /> <Setter Property="Width" Value="2" />
@@ -767,7 +736,7 @@
</Style> </Style>
<Style Selector="Border.task-row Border.chip > StackPanel > TextBlock"> <Style Selector="Border.task-row Border.chip > StackPanel > TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" /> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
</Style> </Style>
@@ -779,12 +748,12 @@
<!-- Diff chip add/del coloring --> <!-- Diff chip add/del coloring -->
<Style Selector="TextBlock.diff-add"> <Style Selector="TextBlock.diff-add">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" /> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}" /> <Setter Property="Foreground" Value="{StaticResource MossBrightBrush}" />
</Style> </Style>
<Style Selector="TextBlock.diff-del"> <Style Selector="TextBlock.diff-del">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" /> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource BloodBrush}" /> <Setter Property="Foreground" Value="{StaticResource BloodBrush}" />
</Style> </Style>
@@ -807,7 +776,7 @@
<!-- LIVE-TAIL PREVIEW ROW --> <!-- LIVE-TAIL PREVIEW ROW -->
<!-- ============================================================ --> <!-- ============================================================ -->
<Style Selector="Border.task-live-tail"> <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="BorderBrush" Value="{StaticResource LineBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" /> <Setter Property="CornerRadius" Value="5" />
@@ -816,7 +785,7 @@
</Style> </Style>
<Style Selector="Border.task-live-tail TextBlock"> <Style Selector="Border.task-live-tail TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <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="Foreground" Value="{StaticResource TextDimBrush}" />
</Style> </Style>
@@ -857,14 +826,23 @@
<Setter Property="Opacity" Value="0.5" /> <Setter Property="Opacity" Value="0.5" />
<Setter Property="TextDecorations" Value="Strikethrough" /> <Setter Property="TextDecorations" Value="Strikethrough" />
</Style> </Style>
<Style Selector="TextBox.subtask-edit">
<Setter Property="Padding" Value="4,2" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
</Style>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- SECTION LABELS (OVERDUE / TASKS / COMPLETED) --> <!-- SECTION LABELS (OVERDUE / TASKS / COMPLETED) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<Style Selector="TextBlock.section-label"> <Style Selector="TextBlock.section-label">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="10" /> <Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="LetterSpacing" Value="1.4" /> <Setter Property="LetterSpacing" Value="1.4" />
</Style> </Style>
<Style Selector="TextBlock.section-label.overdue"> <Style Selector="TextBlock.section-label.overdue">
@@ -881,9 +859,9 @@
</Style> </Style>
<Style Selector="Border.badge > TextBlock"> <Style Selector="Border.badge > TextBlock">
<Setter Property="FontSize" Value="9"/> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}"/>
<Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="White"/> <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
</Style> </Style>
<Style Selector="Border.badge.draft"> <Style Selector="Border.badge.draft">
@@ -898,4 +876,153 @@
<Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/> <Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
</Style> </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> </Styles>

View File

@@ -84,6 +84,19 @@
<SolidColorBrush x:Key="StatusQueuedBrush" Color="{StaticResource StatusQueuedColor}" /> <SolidColorBrush x:Key="StatusQueuedBrush" Color="{StaticResource StatusQueuedColor}" />
<SolidColorBrush x:Key="StatusIdleBrush" Color="{StaticResource StatusIdleColor}" /> <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) --> <!-- Window-body gradient layers (apply as LinearGradientBrush in the main content Border) -->
<LinearGradientBrush x:Key="DesktopBackgroundBrush" StartPoint="0%,0%" EndPoint="0%,100%"> <LinearGradientBrush x:Key="DesktopBackgroundBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#FF05070A" /> <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> <FontFamily x:Key="MonoFamily">avares://ClaudeDo.Ui/Assets/Fonts/#JetBrains Mono, IBM Plex Mono, Cascadia Mono, Consolas, monospace</FontFamily>
<!-- Type scale --> <!-- 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="FontSizeMono">11</x:Double> <!-- chips, log lines, filepaths -->
<x:Double x:Key="FontSizeMicro">11</x:Double> <!-- meta rows --> <x:Double x:Key="FontSizeMicro">11</x:Double> <!-- meta rows -->
<x:Double x:Key="FontSizeBody">13</x:Double> <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="FontSizeH3">18</x:Double>
<x:Double x:Key="FontSizeH2">24</x:Double> <!-- island titles ("My Day") --> <x:Double x:Key="FontSizeH2">24</x:Double> <!-- island titles ("My Day") -->
<x:Double x:Key="FontSizeH1">32</x:Double> <x:Double x:Key="FontSizeH1">32</x:Double>
@@ -162,7 +175,7 @@
<Style x:Key="EyebrowText" Selector="TextBlock.eyebrow"> <Style x:Key="EyebrowText" Selector="TextBlock.eyebrow">
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" /> <Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" /> <Setter Property="FontSize" Value="{StaticResource FontSizeEyebrow}" />
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="LetterSpacing" Value="1.4" /> <Setter Property="LetterSpacing" Value="1.4" />
</Style> </Style>

View File

@@ -450,6 +450,7 @@ public sealed record AppSettingsDto(
string DefaultModel, string DefaultModel,
int DefaultMaxTurns, int DefaultMaxTurns,
string DefaultPermissionMode, string DefaultPermissionMode,
int MaxParallelExecutions,
string WorktreeStrategy, string WorktreeStrategy,
string? CentralWorktreeRoot, string? CentralWorktreeRoot,
bool WorktreeAutoCleanupEnabled, bool WorktreeAutoCleanupEnabled,

View File

@@ -840,6 +840,35 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
CloseDetail?.Invoke(); CloseDetail?.Invoke();
} }
[RelayCommand]
private async System.Threading.Tasks.Task CommitSubtaskEditAsync(SubtaskRowViewModel? row)
{
if (row is null || !row.IsEditing) return;
row.IsEditing = false;
var title = row.Title?.Trim() ?? "";
await using var ctx = _dbFactory.CreateDbContext();
var repo = new SubtaskRepository(ctx);
// Emptying the text removes the step.
if (string.IsNullOrEmpty(title))
{
await repo.DeleteAsync(row.Id);
Subtasks.Remove(row);
return;
}
var subs = await repo.GetByTaskIdAsync(Task?.Id ?? "");
var entity = subs.FirstOrDefault(s => s.Id == row.Id);
if (entity is null) return;
if (entity.Title != title)
{
entity.Title = title;
await repo.UpdateAsync(entity);
}
row.Title = title;
}
[RelayCommand] [RelayCommand]
private async System.Threading.Tasks.Task AddSubtaskAsync() private async System.Threading.Tasks.Task AddSubtaskAsync()
{ {
@@ -943,6 +972,7 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase
public required string Id { get; init; } public required string Id { get; init; }
[ObservableProperty] private string _title = ""; [ObservableProperty] private string _title = "";
[ObservableProperty] private bool _done; [ObservableProperty] private bool _done;
[ObservableProperty] private bool _isEditing;
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status; [ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active; [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
} }

View File

@@ -12,6 +12,8 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
[ObservableProperty] private bool _isActive; [ObservableProperty] private bool _isActive;
[ObservableProperty] private string? _workingDir; [ObservableProperty] private string? _workingDir;
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType; [ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
[ObservableProperty] private bool _dropHintAbove;
[ObservableProperty] private bool _dropHintBelow;
public string? IconKey { get; init; } public string? IconKey { get; init; }
public string? DotColorKey { get; init; } public string? DotColorKey { get; init; }
} }

View File

@@ -82,6 +82,52 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
finally { _worktreesOverviewOpen = false; } finally { _worktreesOverviewOpen = false; }
} }
[RelayCommand]
private void OpenInExplorer(ListNavItemViewModel? row)
{
var dir = row?.WorkingDir;
if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return;
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = dir,
UseShellExecute = true,
});
}
catch { /* best-effort */ }
}
[RelayCommand]
private void OpenInTerminal(ListNavItemViewModel? row)
{
var dir = row?.WorkingDir;
if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return;
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "wt.exe",
Arguments = $"-d \"{dir}\"",
UseShellExecute = true,
});
}
catch
{
// Windows Terminal not installed — fall back to a plain console at the directory.
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "cmd.exe",
WorkingDirectory = dir,
UseShellExecute = true,
});
}
catch { /* best-effort */ }
}
}
public ObservableCollection<ListNavItemViewModel> Items { get; } = new(); public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new(); public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new(); public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();
@@ -231,6 +277,57 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
} }
} }
public void ClearDropHints()
{
foreach (var r in UserLists)
{
r.DropHintAbove = false;
r.DropHintBelow = false;
}
}
public void SetDropHint(ListNavItemViewModel target, bool placeBelow)
{
foreach (var r in UserLists)
{
var isTarget = ReferenceEquals(r, target);
r.DropHintAbove = isTarget && !placeBelow;
r.DropHintBelow = isTarget && placeBelow;
}
}
public async Task ReorderAsync(ListNavItemViewModel source, ListNavItemViewModel target, bool placeBelow)
{
if (source.Kind != ListKind.User || target.Kind != ListKind.User) return;
if (ReferenceEquals(source, target)) return;
MoveWithinCollection(UserLists, source, target, placeBelow);
var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList();
await using var ctx = await _dbFactory.CreateDbContextAsync();
var lists = new ListRepository(ctx);
await lists.ReorderAsync(orderedIds);
}
private static void MoveWithinCollection(
ObservableCollection<ListNavItemViewModel> coll,
ListNavItemViewModel source,
ListNavItemViewModel target,
bool placeBelow)
{
var srcIdx = coll.IndexOf(source);
var tgtIdx = coll.IndexOf(target);
if (srcIdx < 0 || tgtIdx < 0 || srcIdx == tgtIdx) return;
var finalIdx = placeBelow ? tgtIdx + 1 : tgtIdx;
if (srcIdx < finalIdx) finalIdx--;
if (finalIdx < 0) finalIdx = 0;
if (finalIdx >= coll.Count) finalIdx = coll.Count - 1;
if (finalIdx == srcIdx) return;
coll.Move(srcIdx, finalIdx);
}
partial void OnSelectedListChanged(ListNavItemViewModel? value) partial void OnSelectedListChanged(ListNavItemViewModel? value)
{ {
foreach (var i in Items) i.IsActive = ReferenceEquals(i, value); foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);

View File

@@ -17,7 +17,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private PlanningPhase _planningPhase; [ObservableProperty] private PlanningPhase _planningPhase;
[ObservableProperty] private string? _branch; [ObservableProperty] private string? _branch;
[ObservableProperty] private string? _diffStat; [ObservableProperty] private string? _diffStat;
[ObservableProperty] private string? _liveTail;
[ObservableProperty] private DateTime? _scheduledFor; [ObservableProperty] private DateTime? _scheduledFor;
[ObservableProperty] private int _diffAdditions; [ObservableProperty] private int _diffAdditions;
[ObservableProperty] private int _diffDeletions; [ObservableProperty] private int _diffDeletions;
@@ -74,7 +73,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
&& PlanningPhase == PlanningPhase.Finalized && PlanningPhase == PlanningPhase.Finalized
&& !HasQueuedSubtasks; && !HasQueuedSubtasks;
public bool HasSchedule => ScheduledFor.HasValue; public bool HasSchedule => ScheduledFor.HasValue;
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
public string DiffAdditionsText => $"+{DiffAdditions}"; public string DiffAdditionsText => $"+{DiffAdditions}";
public string DiffDeletionsText => $"{DiffDeletions}"; public string DiffDeletionsText => $"{DiffDeletions}";
@@ -96,7 +94,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsQueued));
OnPropertyChanged(nameof(IsWaiting)); OnPropertyChanged(nameof(IsWaiting));
OnPropertyChanged(nameof(HasLiveTail));
OnPropertyChanged(nameof(IsDraft)); OnPropertyChanged(nameof(IsDraft));
OnPropertyChanged(nameof(IsPlanned)); OnPropertyChanged(nameof(IsPlanned));
OnPropertyChanged(nameof(CanOpenPlanningSession)); OnPropertyChanged(nameof(CanOpenPlanningSession));
@@ -152,7 +149,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
} }
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail));
partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue)); partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue));
partial void OnScheduledForChanged(DateTime? value) partial void OnScheduledForChanged(DateTime? value)
{ {

View File

@@ -56,18 +56,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
{ {
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated; _worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated; _worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
_worker.TaskMessageEvent += OnWorkerTaskMessage;
_worker.ListUpdatedEvent += OnWorkerListUpdated; _worker.ListUpdatedEvent += OnWorkerListUpdated;
_worker.ConnectionRestoredEvent += () => LoadForList(_currentList); _worker.ConnectionRestoredEvent += () => LoadForList(_currentList);
} }
} }
private void OnWorkerTaskMessage(string taskId, string line)
{
var row = Items.FirstOrDefault(r => r.Id == taskId);
if (row is not null) row.LiveTail = line;
}
private async void OnWorkerListUpdated(string listId) private async void OnWorkerListUpdated(string listId)
{ {
// Mirror the renamed list onto every task row that references it, // Mirror the renamed list onto every task row that references it,
@@ -487,6 +480,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
TasksChanged?.Invoke(this, EventArgs.Empty); TasksChanged?.Invoke(this, EventArgs.Empty);
} }
[RelayCommand]
private async Task ClearCompletedAsync()
{
if (CompletedItems.Count == 0) return;
// Delete children before parents so the parent-child FK (Restrict) doesn't
// block removing a completed planning parent together with its done children.
var toDelete = CompletedItems.OrderByDescending(r => r.IsChild).ToList();
if (ConfirmAsync is not null)
{
var ok = await ConfirmAsync($"Clear {toDelete.Count} completed task(s)? This cannot be undone.");
if (!ok) return;
}
await using var db = await _dbFactory.CreateDbContextAsync();
var repo = new TaskRepository(db);
foreach (var row in toDelete)
{
try
{
await repo.DeleteAsync(row.Id);
Items.Remove(row);
}
catch { /* still referenced by open child tasks; leave it visible */ }
}
Regroup();
UpdateSubtitle();
TasksChanged?.Invoke(this, EventArgs.Empty);
}
[RelayCommand] [RelayCommand]
private async Task ToggleStarAsync(TaskRowViewModel row) private async Task ToggleStarAsync(TaskRowViewModel row)
{ {

View File

@@ -51,6 +51,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
// Set by MainWindow to open the global worktrees overview dialog. // Set by MainWindow to open the global worktrees overview dialog.
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; } 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 bool _isUpdateBannerVisible;
[ObservableProperty] private string? _updateBannerLatestVersion; [ObservableProperty] private string? _updateBannerLatestVersion;
[ObservableProperty] private string? _inlineUpdateStatus; [ObservableProperty] private string? _inlineUpdateStatus;
@@ -72,6 +75,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
public bool ShowLists => WindowWidth >= 780; public bool ShowLists => WindowWidth >= 780;
private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false }; 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; [ObservableProperty] private string? _primeStatus;
private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false }; private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false };
@@ -220,8 +224,12 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
}; };
_primeStatusTimer.Elapsed += (_, _) => _primeStatusTimer.Elapsed += (_, _) =>
Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null); Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null);
_connectTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() =>
{
if (DecideShowConnectionPrompt(IsOffline)) _ = OpenWorkerConnectionHelpAsync();
});
_connectTimer.Start();
_ = Lists.LoadAsync(); _ = Lists.LoadAsync();
_ = EnsureWorkerRunningAsync();
_updateCheck.PropertyChanged += (_, e) => _updateCheck.PropertyChanged += (_, e) =>
{ {
if (e.PropertyName == nameof(UpdateCheckService.LastCheckStatus)) if (e.PropertyName == nameof(UpdateCheckService.LastCheckStatus))
@@ -270,6 +278,25 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
if (ShowAboutModal is not null) await ShowAboutModal(vm); 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] [RelayCommand]
private async Task OpenRepoImport() private async Task OpenRepoImport()
{ {
@@ -305,20 +332,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
[ObservableProperty] private string? _restartWorkerStatus; [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] [RelayCommand]
private async Task RestartWorkerAsync() private async Task RestartWorkerAsync()
{ {

View File

@@ -9,6 +9,7 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
[ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias; [ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias;
[ObservableProperty] private int _defaultMaxTurns = 100; [ObservableProperty] private int _defaultMaxTurns = 100;
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode; [ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
[ObservableProperty] private int _maxParallelExecutions = 1;
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases; public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes; public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
@@ -17,6 +18,8 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
{ {
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200) if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
return "Max turns must be between 1 and 200."; return "Max turns must be between 1 and 200.";
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
return "Max parallel executions must be between 1 and 20.";
return null; return null;
} }
} }

View File

@@ -42,6 +42,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
General.DefaultModel = dto.DefaultModel ?? "sonnet"; General.DefaultModel = dto.DefaultModel ?? "sonnet";
General.DefaultMaxTurns = dto.DefaultMaxTurns; General.DefaultMaxTurns = dto.DefaultMaxTurns;
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto"; General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
General.MaxParallelExecutions = dto.MaxParallelExecutions;
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling"; Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot; Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled; Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
@@ -69,6 +70,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
General.DefaultModel ?? "sonnet", General.DefaultModel ?? "sonnet",
General.DefaultMaxTurns, General.DefaultMaxTurns,
General.DefaultPermissionMode ?? "auto", General.DefaultPermissionMode ?? "auto",
General.MaxParallelExecutions,
Worktrees.WorktreeStrategy ?? "sibling", Worktrees.WorktreeStrategy ?? "sibling",
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot, string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
Worktrees.WorktreeAutoCleanupEnabled, Worktrees.WorktreeAutoCleanupEnabled,

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

View File

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

View File

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

View File

@@ -1,9 +1,13 @@
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.Views.Modals; using ClaudeDo.Ui.Views.Modals;
using ClaudeDo.Ui.Views.Planning; using ClaudeDo.Ui.Views.Planning;
@@ -135,6 +139,31 @@ public partial class DetailsIslandView : UserControl
return await tcs.Task; return await tcs.Task;
} }
private void OnSubtaskTitleTapped(object? sender, TappedEventArgs e)
{
if (sender is not Control c || c.DataContext is not SubtaskRowViewModel row) return;
row.IsEditing = true;
var box = (c.GetVisualParent() as Panel)?.GetVisualDescendants().OfType<TextBox>().FirstOrDefault();
if (box is not null)
Dispatcher.UIThread.Post(() => { box.Focus(); box.SelectAll(); }, DispatcherPriority.Background);
}
private void OnSubtaskEditLostFocus(object? sender, RoutedEventArgs e)
{
if (DataContext is DetailsIslandViewModel vm
&& sender is Control c && c.DataContext is SubtaskRowViewModel row)
vm.CommitSubtaskEditCommand.Execute(row);
}
private async void OnTaskIdTapped(object? sender, TappedEventArgs e)
{
if (DataContext is not DetailsIslandViewModel vm || vm.Task is null) return;
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard is null) return;
await clipboard.SetTextAsync(vm.Task.Id);
}
private async void OnCopyDescriptionClick(object? sender, RoutedEventArgs e) private async void OnCopyDescriptionClick(object? sender, RoutedEventArgs e)
{ {
if (DataContext is not DetailsIslandViewModel vm) return; if (DataContext is not DetailsIslandViewModel vm) return;

View File

@@ -4,20 +4,12 @@
xmlns:converters="using:ClaudeDo.Ui.Converters" xmlns:converters="using:ClaudeDo.Ui.Converters"
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView" x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
x:DataType="vm:ListsIslandViewModel"> 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"> <DockPanel LastChildFill="True">
<!-- ── Header ── --> <!-- ── Header ── -->
<Border DockPanel.Dock="Top" Classes="island-header"> <Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Margin="14,12,14,0" Spacing="4"> <StackPanel Spacing="4">
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="18" <TextBlock Classes="heading" Text="Lists"/>
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="Lists"/>
<!-- Search row --> <!-- Search row -->
<Border Classes="search-wrap" Margin="0,8,0,12"> <Border Classes="search-wrap" Margin="0,8,0,12">
@@ -45,18 +37,16 @@
<!-- Avatar circle --> <!-- Avatar circle -->
<Border Grid.Column="0" Classes="avatar-circle" <Border Grid.Column="0" Classes="avatar-circle"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock Text="{Binding UserInitials}" <TextBlock Classes="eyebrow"
FontFamily="{DynamicResource MonoFont}" FontSize="10" Text="{Binding UserInitials}"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource DeepBrush}" Foreground="{DynamicResource DeepBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/> HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border> </Border>
<!-- Name + machine --> <!-- Name + machine -->
<StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center">
<TextBlock Text="{Binding UserName}" <TextBlock Classes="title" Text="{Binding UserName}"/>
FontSize="12" Foreground="{DynamicResource TextBrush}"/> <TextBlock Classes="meta">
<TextBlock FontFamily="{DynamicResource MonoFont}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}">
<TextBlock.Text> <TextBlock.Text>
<MultiBinding StringFormat="{}{0} / local"> <MultiBinding StringFormat="{}{0} / local">
<Binding Path="MachineName"/> <Binding Path="MachineName"/>
@@ -80,7 +70,7 @@
<StackPanel Margin="6,0,6,4"> <StackPanel Margin="6,0,6,4">
<!-- SMART LISTS section --> <!-- 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 ItemsSource="{Binding SmartLists}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel"> <DataTemplate DataType="vm:ListNavItemViewModel">
@@ -90,10 +80,8 @@
<!-- Left accent bar for active state --> <!-- Left accent bar for active state -->
<Border Grid.Column="0" Grid.ColumnSpan="3" <Border Grid.Column="0" Grid.ColumnSpan="3"
Background="Transparent" Background="Transparent"
CornerRadius="8" IsHitTestVisible="False"> CornerRadius="8" IsHitTestVisible="False"
<Border.IsVisible> IsVisible="{Binding IsActive}">
<Binding Path="IsActive"/>
</Border.IsVisible>
<Border Width="2" Height="16" <Border Width="2" Height="16"
Background="{DynamicResource AccentBrush}" Background="{DynamicResource AccentBrush}"
CornerRadius="1" CornerRadius="1"
@@ -110,7 +98,7 @@
<TextBlock Grid.Column="1" Classes="list-label" <TextBlock Grid.Column="1" Classes="list-label"
Text="{Binding Name}" Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0" VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="13"/> Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count --> <!-- Count -->
<TextBlock Grid.Column="2" Classes="list-count" <TextBlock Grid.Column="2" Classes="list-count"
Text="{Binding Count}"/> Text="{Binding Count}"/>
@@ -121,12 +109,22 @@
</ItemsControl> </ItemsControl>
<!-- MY LISTS section --> <!-- 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 ItemsSource="{Binding UserLists}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel"> <DataTemplate DataType="vm:ListNavItemViewModel">
<Border Classes="list-item" Classes.active="{Binding IsActive}" <Grid RowDefinitions="Auto,Auto,Auto">
Tapped="OnItemTapped">
<!-- Above-row drop indicator -->
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintAbove}"/>
<Border Grid.Row="1" Classes="list-item" Classes.active="{Binding IsActive}"
Tapped="OnItemTapped"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnListDragOver"
DragDrop.Drop="OnListDrop">
<Border.ContextMenu> <Border.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="Settings..." <MenuItem Header="Settings..."
@@ -135,6 +133,15 @@
<MenuItem Header="Worktrees…" <MenuItem Header="Worktrees…"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}" Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
<Separator IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<MenuItem Header="Open in Explorer"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInExplorerCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="Open in Terminal"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInTerminalCommand}"
CommandParameter="{Binding}"/>
</ContextMenu> </ContextMenu>
</Border.ContextMenu> </Border.ContextMenu>
<Grid ColumnDefinitions="20,*,Auto"> <Grid ColumnDefinitions="20,*,Auto">
@@ -158,12 +165,18 @@
<TextBlock Grid.Column="1" Classes="list-label" <TextBlock Grid.Column="1" Classes="list-label"
Text="{Binding Name}" Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0" VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextDimBrush}" FontSize="13"/> Foreground="{DynamicResource TextDimBrush}" FontSize="{StaticResource FontSizeBody}"/>
<!-- Count --> <!-- Count -->
<TextBlock Grid.Column="2" Classes="list-count" <TextBlock Grid.Column="2" Classes="list-count"
Text="{Binding Count}"/> Text="{Binding Count}"/>
</Grid> </Grid>
</Border> </Border>
<!-- Below-row drop indicator (last item only) -->
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintBelow}"/>
</Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
@@ -177,7 +190,8 @@
Width="13" Height="13" Width="13" Height="13"
Foreground="{DynamicResource TextMuteBrush}" Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<TextBlock Text="New list" FontSize="12" <TextBlock Classes="body"
Text="New list"
Foreground="{DynamicResource TextMuteBrush}" Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </StackPanel>

View File

@@ -1,9 +1,11 @@
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
@@ -13,9 +15,13 @@ namespace ClaudeDo.Ui.Views.Islands;
public partial class ListsIslandView : UserControl public partial class ListsIslandView : UserControl
{ {
private static readonly DataFormat<string> ListRowFormat =
DataFormat.CreateStringApplicationFormat("claudedo-list-row");
public ListsIslandView() public ListsIslandView()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel);
DataContextChanged += (_, _) => DataContextChanged += (_, _) =>
{ {
if (DataContext is ListsIslandViewModel vm) if (DataContext is ListsIslandViewModel vm)
@@ -84,6 +90,127 @@ public partial class ListsIslandView : UserControl
vm.SelectCommand.Execute(item); vm.SelectCommand.Execute(item);
} }
private async void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is not ListsIslandViewModel vm) return;
if (e.Source is not Visual src) return;
var border = FindListItemBorder(src);
if (border?.DataContext is not ListNavItemViewModel row || row.Kind != ListKind.User) return;
if (!e.GetCurrentPoint(border).Properties.IsLeftButtonPressed) return;
// Double-click opens the list's settings instead of starting a drag. Handled here
// because DoDragDropAsync captures the pointer and would swallow a DoubleTapped event.
if (e.ClickCount == 2)
{
vm.OpenListSettingsCommand.Execute(row);
return;
}
// Select now so the right pane updates whether the gesture becomes a click or a drag
// (the Tapped handler doesn't fire once DoDragDropAsync captures the pointer).
vm.SelectCommand.Execute(row);
var data = new DataTransfer();
data.Add(DataTransferItem.Create(ListRowFormat, row.Id));
try
{
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
}
finally
{
vm.ClearDropHints();
}
}
private void OnListDragOver(object? sender, DragEventArgs e)
{
if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
if (!e.DataTransfer?.Contains(ListRowFormat) ?? true)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2;
// Canonicalize: "drop below X" == "drop above X+1". Only the last row shows a below-line.
ListNavItemViewModel hintRow = target;
bool hintBelow = false;
if (placeBelow)
{
var next = FindNextUserList(vm, target);
if (next is not null) { hintRow = next; hintBelow = false; }
else { hintRow = target; hintBelow = true; }
}
if (hintRow.Id == sourceId)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
vm.SetDropHint(hintRow, hintBelow);
e.DragEffects = DragDropEffects.Move;
}
private async void OnListDrop(object? sender, DragEventArgs e)
{
if (DataContext is not ListsIslandViewModel vm) return;
try
{
if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User) return;
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id) return;
var source = vm.UserLists.FirstOrDefault(r => r.Id == sourceId);
if (source is null) return;
var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2;
vm.ClearDropHints();
await vm.ReorderAsync(source, target, placeBelow);
}
catch
{
vm.ClearDropHints();
throw;
}
}
private static Border? FindListItemBorder(Visual? v)
{
while (v is not null)
{
if (v is Border b && b.Classes.Contains("list-item")) return b;
v = v.GetVisualParent();
}
return null;
}
private static ListNavItemViewModel? FindNextUserList(ListsIslandViewModel vm, ListNavItemViewModel row)
{
var idx = vm.UserLists.IndexOf(row);
if (idx < 0) return null;
return idx + 1 < vm.UserLists.Count ? vm.UserLists[idx + 1] : null;
}
private async System.Threading.Tasks.Task ShowSettingsAsync(SettingsModalViewModel settingsVm) private async System.Threading.Tasks.Task ShowSettingsAsync(SettingsModalViewModel settingsVm)
{ {
var owner = TopLevel.GetTopLevel(this) as Window; var owner = TopLevel.GetTopLevel(this) as Window;

View File

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

View File

@@ -1,6 +1,5 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.Islands; namespace ClaudeDo.Ui.Views.Islands;
@@ -9,16 +8,29 @@ public partial class SessionTerminalView : UserControl
{ {
public SessionTerminalView() { InitializeComponent(); } public SessionTerminalView() { InitializeComponent(); }
private DetailsIslandViewModel? _boundVm;
protected override void OnDataContextChanged(EventArgs e) protected override void OnDataContextChanged(EventArgs e)
{ {
base.OnDataContextChanged(e); base.OnDataContextChanged(e);
if (DataContext is DetailsIslandViewModel vm) if (_boundVm is not null)
vm.Log.CollectionChanged += OnLogChanged; _boundVm.Log.CollectionChanged -= OnLogChanged;
_boundVm = DataContext as DetailsIslandViewModel;
if (_boundVm is not null)
_boundVm.Log.CollectionChanged += OnLogChanged;
} }
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (e.Action != NotifyCollectionChangedAction.Add) return; if (e.Action != NotifyCollectionChangedAction.Add) return;
Dispatcher.UIThread.Post(() => LogScroll.ScrollToEnd(), DispatcherPriority.Background); // Scroll after the next layout pass so the freshly-added (wrapping) line
// is measured first — otherwise ScrollToEnd stops short and clips it.
EventHandler? handler = null;
handler = (_, _) =>
{
LogScroll.LayoutUpdated -= handler;
LogScroll.ScrollToEnd();
};
LogScroll.LayoutUpdated += handler;
} }
} }

View File

@@ -79,9 +79,9 @@
Width="18" Height="18" Width="18" Height="18"
VerticalAlignment="Center"> VerticalAlignment="Center">
<Panel> <Panel>
<TextBlock Text="▾" FontSize="10" IsVisible="{Binding IsExpanded}" <TextBlock Classes="meta" Text="▾" IsVisible="{Binding IsExpanded}"
VerticalAlignment="Center" HorizontalAlignment="Center"/> VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="▸" FontSize="10" IsVisible="{Binding !IsExpanded}" <TextBlock Classes="meta" Text="▸" IsVisible="{Binding !IsExpanded}"
VerticalAlignment="Center" HorizontalAlignment="Center"/> VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Panel> </Panel>
</Button> </Button>
@@ -100,7 +100,7 @@
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center"> <Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
Classes="task-title" Classes="task-title"
Text="{Binding Title}" FontSize="14" Text="{Binding Title}" FontSize="{StaticResource FontSizeTaskTitle}"
Foreground="{DynamicResource TextBrush}" Foreground="{DynamicResource TextBrush}"
TextWrapping="Wrap" TextWrapping="Wrap"
FontStyle="{Binding IsDraft, Converter={StaticResource BoolToItalic}}" FontStyle="{Binding IsDraft, Converter={StaticResource BoolToItalic}}"
@@ -175,20 +175,6 @@
</Border> </Border>
</StackPanel> </StackPanel>
<!-- Live-tail row (visible when running + has tail) -->
<Border Classes="task-live-tail" IsVisible="{Binding HasLiveTail}">
<StackPanel Spacing="3">
<TextBlock Text="{Binding LiveTail}"
TextTrimming="CharacterEllipsis" MaxLines="1"/>
<Grid Height="3" HorizontalAlignment="Stretch">
<Rectangle Fill="{DynamicResource Surface3Brush}"
HorizontalAlignment="Stretch" RadiusX="1.5" RadiusY="1.5"/>
<Rectangle Fill="{DynamicResource MossBrush}"
HorizontalAlignment="Left" Width="60" RadiusX="1.5" RadiusY="1.5"/>
</Grid>
</StackPanel>
</Border>
</StackPanel> </StackPanel>
<!-- Star toggle --> <!-- Star toggle -->
@@ -218,24 +204,22 @@
<Button.Flyout> <Button.Flyout>
<Flyout Placement="Bottom" ShowMode="Standard"> <Flyout Placement="Bottom" ShowMode="Standard">
<Border Background="{DynamicResource Surface2Brush}" <Border Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource BorderBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="1" CornerRadius="10" BorderThickness="1" CornerRadius="10"
Padding="16" Width="300"> Padding="16" Width="300">
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="Schedule task" <TextBlock Classes="title" Text="Schedule task"/>
FontWeight="SemiBold" FontSize="13"
Foreground="{DynamicResource TextBrush}"/>
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock Text="WHEN" FontSize="10" Opacity="0.6" <TextBlock Classes="eyebrow" Text="WHEN"
Foreground="{DynamicResource TextDimBrush}"/> Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
<ctl:ThemedDatePicker x:Name="ScheduleDate" ShowTime="True" <ctl:ThemedDatePicker x:Name="ScheduleDate" ShowTime="True"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" <StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,4,0,0"> 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"/> <Button Content="Schedule" Classes="accent" Click="OnScheduleSetClick" MinWidth="76"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -9,14 +9,13 @@
<!-- Header --> <!-- Header -->
<Border DockPanel.Dock="Top" Classes="island-header"> <Border DockPanel.Dock="Top" Classes="island-header">
<Grid ColumnDefinitions="*,Auto" Margin="18,14"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="eyebrow" Text="{Binding HeaderEyebrow}"/> <TextBlock Classes="eyebrow" Text="{Binding HeaderEyebrow}"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="24" <TextBlock Classes="display"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="{Binding HeaderTitle}" Text="{Binding HeaderTitle}"
TextTrimming="CharacterEllipsis"/> TextTrimming="CharacterEllipsis"/>
<TextBlock FontFamily="{DynamicResource MonoFamily}" FontSize="11" <TextBlock Classes="meta"
Foreground="{DynamicResource TextMuteBrush}" Foreground="{DynamicResource TextMuteBrush}"
Text="{Binding Subtitle}" Text="{Binding Subtitle}"
TextTrimming="CharacterEllipsis"/> TextTrimming="CharacterEllipsis"/>
@@ -127,8 +126,17 @@
<Binding Path="IsShowingCompleted"/> <Binding Path="IsShowingCompleted"/>
</MultiBinding> </MultiBinding>
</StackPanel.IsVisible> </StackPanel.IsVisible>
<TextBlock Classes="eyebrow section-label" <Grid ColumnDefinitions="*,Auto" Margin="14,14,14,6">
Text="{Binding CompletedHeader}" Margin="14,14,14,6"/> <TextBlock Grid.Column="0" Classes="eyebrow section-label"
Text="{Binding CompletedHeader}" VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn"
Command="{Binding ClearCompletedCommand}"
ToolTip.Tip="Clear all completed"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.Trash}" Width="13" Height="13"
Foreground="{DynamicResource BloodBrush}"/>
</Button>
</Grid>
<ItemsControl ItemsSource="{Binding CompletedItems}"> <ItemsControl ItemsSource="{Binding CompletedItems}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskRowViewModel"> <DataTemplate DataType="vm:TaskRowViewModel">

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ using Avalonia;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
@@ -43,10 +42,4 @@ public partial class DiffModalView : Window
Opacity = 1; Opacity = 1;
RenderTransform = new ScaleTransform(1.0, 1.0); 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,14 +1,16 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals" xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView" x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView"
x:DataType="vm:ListSettingsModalViewModel" x:DataType="vm:ListSettingsModalViewModel"
Title="List settings" Title="List settings"
Width="520" Height="720" Width="520" Height="720"
CanResize="True" CanResize="True"
MinWidth="460" MinHeight="520" MinWidth="460" MinHeight="520"
WindowDecorations="None" WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}"> Background="{DynamicResource SurfaceBrush}">
@@ -17,68 +19,21 @@
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/> <KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<Window.Styles> <ctl:ModalShell Title="LIST SETTINGS" CloseCommand="{Binding CancelCommand}">
<Style Selector="TextBlock.section-label"> <ctl:ModalShell.Footer>
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/> <Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Setter Property="FontSize" Value="10"/> <Button Grid.Column="0" Content="Delete list" Classes="danger"
<Setter Property="LetterSpacing" Value="1.4"/> Command="{Binding DeleteCommand}" MinWidth="90"/>
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/> <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Setter Property="Margin" Value="4,0,0,6"/> <Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
</Style> <Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
<Style Selector="TextBlock.field-label"> </StackPanel>
<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"/>
</Grid> </Grid>
</Border> </ctl:ModalShell.Footer>
<!-- Body --> <!-- Body -->
<ScrollViewer Grid.Row="1" Padding="20,16"> <ScrollViewer Padding="20,16">
<StackPanel Spacing="18"> <StackPanel Spacing="12">
<!-- GENERAL --> <!-- GENERAL -->
<StackPanel Spacing="0"> <StackPanel Spacing="0">
@@ -94,7 +49,7 @@
<TextBlock Classes="field-label" Text="Working directory"/> <TextBlock Classes="field-label" Text="Working directory"/>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="(none)" /> <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> </Grid>
</StackPanel> </StackPanel>
@@ -112,7 +67,7 @@
<StackPanel Spacing="0"> <StackPanel Spacing="0">
<Grid ColumnDefinitions="*,Auto" Margin="4,0,0,6"> <Grid ColumnDefinitions="*,Auto" Margin="4,0,0,6">
<TextBlock Classes="section-label" Text="AGENT" Margin="0"/> <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}" /> Command="{Binding ResetAgentSettingsCommand}" />
</Grid> </Grid>
<Border Classes="section"> <Border Classes="section">
@@ -141,22 +96,16 @@
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Name}" <TextBlock Classes="title" Text="{Binding Name}"/>
Foreground="{DynamicResource TextBrush}"/> <TextBlock Classes="meta" Text="{Binding Description}"/>
<TextBlock Text="{Binding Description}"
Foreground="{DynamicResource TextMuteBrush}"
FontSize="11" />
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<Button Grid.Column="1" Content="Browse..." <Button Classes="btn" Grid.Column="1" Content="Browse..."
Margin="8,0,0,0" Click="BrowseAgentClicked" /> Margin="8,0,0,0" Click="BrowseAgentClicked" />
</Grid> </Grid>
<TextBlock Text="{Binding SelectedAgent.Path}" <TextBlock Classes="path-mono" Text="{Binding SelectedAgent.Path}"
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextFaintBrush}"
TextTrimming="PrefixCharacterEllipsis" TextTrimming="PrefixCharacterEllipsis"
IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
@@ -167,21 +116,5 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- Footer --> </ctl:ModalShell>
<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>
</Window> </Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
@@ -14,12 +13,6 @@ public partial class ListSettingsModalView : Window
InitializeComponent(); 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) private async void BrowseAgentClicked(object? sender, RoutedEventArgs e)
{ {
if (DataContext is not ListSettingsModalViewModel vm) return; if (DataContext is not ListSettingsModalViewModel vm) return;

View File

@@ -1,13 +1,15 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals" xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.MergeModalView" x:Class="ClaudeDo.Ui.Views.Modals.MergeModalView"
x:DataType="vm:MergeModalViewModel" x:DataType="vm:MergeModalViewModel"
Title="Merge worktree" Title="Merge worktree"
Width="560" Height="460" Width="560" Height="460" MinWidth="460" MinHeight="360"
CanResize="False" CanResize="True"
WindowDecorations="None" WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}"> Background="{DynamicResource SurfaceBrush}">
@@ -15,54 +17,22 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/> <KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<Window.Styles> <ctl:ModalShell Title="MERGE WORKTREE" CloseCommand="{Binding CancelCommand}">
<Style Selector="TextBlock.field-label"> <ctl:ModalShell.Footer>
<Setter Property="FontSize" Value="11"/> <StackPanel Orientation="Horizontal" Spacing="8"
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/> HorizontalAlignment="Right" VerticalAlignment="Center">
<Setter Property="Margin" Value="0,0,0,4"/> <Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
</Style> <Button Content="Merge" Classes="primary"
<Style Selector="Button.primary"> Command="{Binding SubmitCommand}"
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/> IsDefault="True" MinWidth="90"/>
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/> </StackPanel>
<Setter Property="FontWeight" Value="SemiBold"/> </ctl:ModalShell.Footer>
</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>
<!-- Body --> <!-- Body -->
<ScrollViewer Grid.Row="1" Padding="20,16"> <ScrollViewer Padding="20,16">
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="{Binding TaskTitle, StringFormat='Merging: {0}'}" <TextBlock Classes="title" Text="{Binding TaskTitle, StringFormat='Merging: {0}'}" />
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}" />
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Target branch"/> <TextBlock Classes="field-label" Text="Target branch"/>
@@ -90,22 +60,14 @@
TextWrapping="Wrap" TextWrapping="Wrap"
IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" /> IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Border BorderBrush="{DynamicResource BloodBrush}" <Border Classes="danger-box"
BorderThickness="1"
CornerRadius="6"
Padding="12,10"
IsVisible="{Binding HasConflict}"> IsVisible="{Binding HasConflict}">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Conflicted files:" <TextBlock Classes="title" Text="Conflicted files:" />
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}" />
<ItemsControl ItemsSource="{Binding ConflictFiles}"> <ItemsControl ItemsSource="{Binding ConflictFiles}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding}" <TextBlock Classes="meta" Text="{Binding}" />
FontFamily="{DynamicResource MonoFont}"
FontSize="11"
Foreground="{DynamicResource TextDimBrush}" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
@@ -118,21 +80,5 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- Footer --> </ctl:ModalShell>
<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>
</Window> </Window>

View File

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

View File

@@ -7,9 +7,11 @@
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView" x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
x:DataType="vm:SettingsModalViewModel" x:DataType="vm:SettingsModalViewModel"
Title="Settings" Title="Settings"
Width="580" Height="760" Width="580" Height="760" MinWidth="480" MinHeight="520"
WindowDecorations="None" CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}"> Background="{DynamicResource SurfaceBrush}">
@@ -22,71 +24,28 @@
<conv:TimeSpanToHhmmConverter x:Key="TimeSpanToHhmm"/> <conv:TimeSpanToHhmmConverter x:Key="TimeSpanToHhmm"/>
</Window.Resources> </Window.Resources>
<Window.Styles> <ctl:ModalShell Title="SETTINGS" CloseCommand="{Binding CancelCommand}">
<Style Selector="TextBlock.section-label"> <ctl:ModalShell.Footer>
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/> <StackPanel Orientation="Horizontal" Spacing="8"
<Setter Property="FontSize" Value="10"/> HorizontalAlignment="Right" VerticalAlignment="Center">
<Setter Property="LetterSpacing" Value="1.4"/> <Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/> <Button Content="Save" Classes="primary"
<Setter Property="Margin" Value="4,0,0,6"/> Command="{Binding SaveCommand}"
</Style> IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
<Style Selector="TextBlock.field-label"> </StackPanel>
<Setter Property="FontSize" Value="11"/> </ctl:ModalShell.Footer>
<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>
<!-- Body: tabs + bottom validation/status strip --> <!-- Body: tabs + bottom validation/status strip -->
<DockPanel Grid.Row="1"> <DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2"> <StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2">
<TextBlock Text="{Binding ValidationError}" <TextBlock Classes="meta" Text="{Binding ValidationError}"
Foreground="{DynamicResource BloodBrush}" FontSize="11" Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding ValidationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding ValidationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding StatusMessage}" <TextBlock Classes="meta" Text="{Binding StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
<TabControl Padding="20,12" TabStripPlacement="Top"> <TabControl Padding="20,16" TabStripPlacement="Top">
<TabItem Header="General"> <TabItem Header="General">
<ScrollViewer> <ScrollViewer>
@@ -116,6 +75,14 @@
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Max parallel executions"/>
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
Minimum="1" Maximum="20" Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="140"/>
<TextBlock Text="How many queued tasks the worker runs at once."
Opacity="0.6" FontSize="12"/>
</StackPanel>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -143,11 +110,11 @@
<NumericUpDown Value="{Binding Worktrees.WorktreeAutoCleanupDays, Mode=TwoWay}" <NumericUpDown Value="{Binding Worktrees.WorktreeAutoCleanupDays, Mode=TwoWay}"
Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0" Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0"
IsEnabled="{Binding Worktrees.WorktreeAutoCleanupEnabled}"/> IsEnabled="{Binding Worktrees.WorktreeAutoCleanupEnabled}"/>
<TextBlock Text="days" VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/> <TextBlock Classes="body" Text="days" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/> <Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<Button Content="Cleanup finished worktrees" <Button Classes="btn" Content="Cleanup finished worktrees"
Command="{Binding Worktrees.CleanupWorktreesCommand}" Command="{Binding Worktrees.CleanupWorktreesCommand}"
HorizontalAlignment="Left"/> HorizontalAlignment="Left"/>
<StackPanel> <StackPanel>
@@ -155,14 +122,13 @@
Command="{Binding Worktrees.RequestResetConfirmCommand}" Command="{Binding Worktrees.RequestResetConfirmCommand}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
IsVisible="{Binding !Worktrees.ShowResetConfirm}"/> IsVisible="{Binding !Worktrees.ShowResetConfirm}"/>
<Border BorderBrush="{DynamicResource BloodBrush}" BorderThickness="1" <Border Classes="danger-box"
CornerRadius="6" Padding="12,10"
IsVisible="{Binding Worktrees.ShowResetConfirm}"> IsVisible="{Binding Worktrees.ShowResetConfirm}">
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost." <TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost."
Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/> Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8"> <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" <Button Content="Remove All" Classes="danger"
Command="{Binding Worktrees.ConfirmResetAllCommand}"/> Command="{Binding Worktrees.ConfirmResetAllCommand}"/>
</StackPanel> </StackPanel>
@@ -170,8 +136,7 @@
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding Worktrees.StatusMessage}" <TextBlock Classes="meta" Text="{Binding Worktrees.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
IsVisible="{Binding Worktrees.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding Worktrees.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
@@ -179,13 +144,12 @@
<TabItem Header="Files"> <TabItem Header="Files">
<ScrollViewer> <ScrollViewer>
<StackPanel Spacing="14" Margin="0,8,0,0"> <StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="AGENTS"/> <TextBlock Classes="section-label" Text="AGENTS"/>
<TextBlock Text="Restore bundled default agents. Existing files are not overwritten." <TextBlock Classes="meta" Text="Restore bundled default agents. Existing files are not overwritten."
FontSize="11" TextWrapping="Wrap" TextWrapping="Wrap"/>
Foreground="{DynamicResource TextDimBrush}"/> <Button Classes="btn" Content="Restore default agents"
<Button Content="Restore default agents"
Command="{Binding Files.RestoreDefaultAgentsCommand}" Command="{Binding Files.RestoreDefaultAgentsCommand}"
IsEnabled="{Binding !Files.IsBusy}" IsEnabled="{Binding !Files.IsBusy}"
HorizontalAlignment="Left"/> HorizontalAlignment="Left"/>
@@ -195,20 +159,19 @@
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8"> <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="0" Classes="field-label" Text="System" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" 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"/> 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="0" Classes="field-label" Text="Planning" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" 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"/> 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="0" Classes="field-label" Text="Agent" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.AgentPromptPath}" 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"/> Command="{Binding Files.OpenPromptCommand}" CommandParameter="Agent"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding Files.StatusMessage}" <TextBlock Classes="meta" Text="{Binding Files.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
IsVisible="{Binding Files.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> IsVisible="{Binding Files.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
@@ -217,8 +180,7 @@
<TabItem Header="Prime Claude"> <TabItem Header="Prime Claude">
<ScrollViewer> <ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0"> <StackPanel Spacing="12" Margin="0,8,0,0">
<TextBlock TextWrapping="Wrap" FontSize="11" <TextBlock Classes="meta" TextWrapping="Wrap"
Foreground="{DynamicResource TextDimBrush}"
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."/> 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 ItemsSource="{Binding Prime.Rows}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
@@ -239,10 +201,9 @@
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<CheckBox Grid.Column="3" Content="MonFri" <CheckBox Grid.Column="3" Content="MonFri"
IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/> IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="4" Text="{Binding LastRunLabel}" VerticalAlignment="Center" <TextBlock Classes="meta" Grid.Column="4" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
MinWidth="80"/> MinWidth="80"/>
<Button Grid.Column="5" Content="✕" <Button Classes="icon-btn" Grid.Column="5" Content="✕"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}" Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
</Grid> </Grid>
@@ -250,7 +211,7 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<Button Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/> <Button Classes="btn" Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
@@ -258,21 +219,5 @@
</TabControl> </TabControl>
</DockPanel> </DockPanel>
<!-- Footer --> </ctl:ModalShell>
<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>
</Window> </Window>

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals; namespace ClaudeDo.Ui.Views.Modals;
@@ -17,10 +16,4 @@ public partial class SettingsModalView : Window
if (DataContext is SettingsModalViewModel vm) if (DataContext is SettingsModalViewModel vm)
vm.CloseAction = Close; 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" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals" xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.UnfinishedPlanningModalView" x:Class="ClaudeDo.Ui.Views.Modals.UnfinishedPlanningModalView"
x:DataType="vm:UnfinishedPlanningModalViewModel" x:DataType="vm:UnfinishedPlanningModalViewModel"
Title="Unfinished planning session" Title="Unfinished planning session"
@@ -15,68 +16,25 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/> <KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<Window.Styles> <ctl:ModalShell Title="UNFINISHED PLANNING SESSION" CloseCommand="{Binding CancelCommand}">
<Style Selector="Button.primary"> <ctl:ModalShell.Footer>
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/> <StackPanel Orientation="Horizontal" Spacing="8"
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/> HorizontalAlignment="Right" VerticalAlignment="Center">
<Setter Property="FontWeight" Value="SemiBold"/> <Button Classes="btn" Content="Discard" Command="{Binding DiscardCommand}" MinWidth="80"/>
</Style> <Button Classes="btn" Content="Finalize" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
</Window.Styles> <Button Content="Resume" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
</StackPanel>
<Border Background="{DynamicResource SurfaceBrush}" </ctl:ModalShell.Footer>
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>
<!-- Body --> <!-- Body -->
<StackPanel Grid.Row="1" Margin="20,16" Spacing="8"> <StackPanel Margin="20,16" Spacing="8">
<TextBlock Text="{Binding TaskTitle}" <TextBlock Classes="title" Text="{Binding TaskTitle}"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}"
TextTrimming="CharacterEllipsis"/> TextTrimming="CharacterEllipsis"/>
<TextBlock Foreground="{DynamicResource TextDimBrush}"> <TextBlock Classes="body">
<Run Text="{Binding DraftCount}"/> <Run Text="{Binding DraftCount}"/>
<Run Text=" draft task(s) waiting to be finalized."/> <Run Text=" draft task(s) waiting to be finalized."/>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<!-- Footer --> </ctl:ModalShell>
<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>
</Window> </Window>

View File

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

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:converters="using:ClaudeDo.Ui.Converters" xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Modals.WorktreesOverviewModalView" x:Class="ClaudeDo.Ui.Views.Modals.WorktreesOverviewModalView"
x:DataType="vm:WorktreesOverviewModalViewModel" x:DataType="vm:WorktreesOverviewModalViewModel"
Title="{Binding Title}" Title="{Binding Title}"
@@ -16,7 +17,7 @@
<Window.Resources> <Window.Resources>
<converters:WorktreeStateColorConverter x:Key="WorktreeStateColor"/> <converters:WorktreeStateColorConverter x:Key="WorktreeStateColor"/>
<DataTemplate x:Key="WorktreeRowTemplate" x:DataType="vm:WorktreeOverviewRowViewModel"> <DataTemplate x:Key="WorktreeRowTemplate" x:DataType="vm:WorktreeOverviewRowViewModel">
<Border Classes="wt-row" <Border Classes="task-row"
Classes.selected="{Binding IsSelected}" Classes.selected="{Binding IsSelected}"
Tapped="OnRowTapped"> Tapped="OnRowTapped">
<Border.ContextMenu> <Border.ContextMenu>
@@ -53,120 +54,63 @@
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
<Separator/> <Separator/>
<MenuItem Header="Force remove" <MenuItem Header="Force remove"
Foreground="#EF5350" Foreground="{DynamicResource StatusErrorBrush}"
Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ForceRemoveCommand}" Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ForceRemoveCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
</ContextMenu> </ContextMenu>
</Border.ContextMenu> </Border.ContextMenu>
<Grid ColumnDefinitions="*,90,80,80"> <Grid ColumnDefinitions="*,90,80,80">
<StackPanel Grid.Column="0" Orientation="Vertical" Spacing="2"> <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"> <StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding TaskStatus}" FontSize="10" <TextBlock Classes="meta" Text="{Binding TaskStatus}"/>
Foreground="{DynamicResource TextFaintBrush}"/> <TextBlock Classes="meta" Text="•"
<TextBlock Text="•" FontSize="10" Foreground="{DynamicResource TextFaintBrush}"
IsVisible="{Binding !PathExistsOnDisk}"/> IsVisible="{Binding !PathExistsOnDisk}"/>
<TextBlock Text="phantom" FontSize="10" Foreground="#EF5350" <TextBlock Classes="meta" Text="phantom" Foreground="{DynamicResource StatusErrorBrush}"
IsVisible="{Binding !PathExistsOnDisk}" IsVisible="{Binding !PathExistsOnDisk}"
ToolTip.Tip="Directory missing on disk"/> ToolTip.Tip="Directory missing on disk"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<Border Grid.Column="1" CornerRadius="3" Padding="6,2" VerticalAlignment="Center" <Border Grid.Column="1" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
Background="{Binding State, Converter={StaticResource WorktreeStateColor}}"> 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"/> HorizontalAlignment="Center"/>
</Border> </Border>
<TextBlock Grid.Column="2" Text="{Binding DiffStat}" VerticalAlignment="Center" <TextBlock Grid.Column="2" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/>
FontFamily="{DynamicResource MonoFont}" FontSize="11" <TextBlock Grid.Column="3" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/>
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Grid.Column="3" Text="{Binding AgeText}" VerticalAlignment="Center"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
</Window.Resources> </Window.Resources>
<Window.Styles> <ctl:ModalShell Title="{Binding Title}" CloseCommand="{Binding CloseCommand}">
<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>
<Grid RowDefinitions="36,Auto,*,52"> <!-- Body: toolbar + content -->
<DockPanel>
<!-- 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>
<!-- Toolbar --> <!-- Toolbar -->
<Border Grid.Row="1" <Border DockPanel.Dock="Top"
Background="{DynamicResource DeepBrush}" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Padding="12,8"> Padding="12,8">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/> <Button Classes="btn" Content="Refresh" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
<Button Content="Cleanup finished" Command="{Binding CleanupFinishedCommand}" 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" <TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="12,0,0,0"
Foreground="{DynamicResource TextDimBrush}"/> Foreground="{DynamicResource TextDimBrush}"/>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- Content --> <!-- Content -->
<ScrollViewer Grid.Row="2" Padding="12,8"> <ScrollViewer Padding="20,16">
<StackPanel> <StackPanel>
<!-- Column headers --> <!-- Column headers -->
<Grid ColumnDefinitions="*,90,80,80" Margin="12,0,12,4"> <Grid ColumnDefinitions="*,90,80,80" Margin="12,0,12,4">
<TextBlock Grid.Column="0" Text="TASK" <TextBlock Grid.Column="0" Classes="eyebrow" Text="TASK"/>
FontFamily="{DynamicResource MonoFont}" FontSize="10" LetterSpacing="1.4" <TextBlock Grid.Column="1" Classes="eyebrow" Text="STATE"/>
Foreground="{DynamicResource TextFaintBrush}"/> <TextBlock Grid.Column="2" Classes="eyebrow" Text="DIFF"/>
<TextBlock Grid.Column="1" Text="STATE" <TextBlock Grid.Column="3" Classes="eyebrow" Text="AGE"/>
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}"/>
</Grid> </Grid>
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8"/> <Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8"/>
@@ -183,7 +127,8 @@
<ItemsControl ItemsSource="{Binding Groups}" IsVisible="{Binding IsGlobal}"> <ItemsControl ItemsSource="{Binding Groups}" IsVisible="{Binding IsGlobal}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreesGroupViewModel"> <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 ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:WorktreeOverviewRowViewModel"> <DataTemplate DataType="vm:WorktreeOverviewRowViewModel">
@@ -198,16 +143,7 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- Footer --> </DockPanel>
<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>
</Grid> </ctl:ModalShell>
</Window> </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,65 +1,51 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Planning" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Planning"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:DataType="vm:ConflictResolutionViewModel" x:DataType="vm:ConflictResolutionViewModel"
x:Class="ClaudeDo.Ui.Views.Planning.ConflictResolutionView" x:Class="ClaudeDo.Ui.Views.Planning.ConflictResolutionView"
Title="Merge conflict" Title="Merge conflict"
Width="560" SizeToContent="Height" Width="560" SizeToContent="Height" MinWidth="460"
WindowDecorations="None" CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="{StaticResource SurfaceBrush}"> Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings> <Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding AbortCommand}"/> <KeyBinding Gesture="Escape" Command="{Binding AbortCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<Border Background="{StaticResource SurfaceBrush}" <ctl:ModalShell Title="MERGE CONFLICT" CloseCommand="{Binding AbortCommand}">
BorderBrush="{StaticResource LineBrush}" <ctl:ModalShell.Footer>
BorderThickness="1"> <StackPanel Orientation="Horizontal" Spacing="8"
<Grid RowDefinitions="36,*"> HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Open all in VS Code" Command="{Binding OpenInVsCodeCommand}"/>
<!-- Title bar / drag handle --> <Button Classes="btn" Content="I've resolved — continue" Command="{Binding ContinueCommand}"/>
<Border Grid.Row="0" <Button Classes="btn" Content="Abort this merge" Command="{Binding AbortCommand}"/>
x:Name="TitleBar" </StackPanel>
Background="{StaticResource Surface2Brush}" </ctl:ModalShell.Footer>
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>
<!-- Content --> <!-- Content -->
<StackPanel Grid.Row="1" Spacing="12" Margin="16" MinWidth="520"> <StackPanel Spacing="12" Margin="20,16" MinWidth="520">
<TextBlock FontWeight="SemiBold" FontSize="16" <TextBlock Classes="heading"
Text="{Binding SubtaskTitle, StringFormat='Conflicts in subtask: {0}'}"/> 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 ItemsSource="{Binding ConflictedFiles}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding}" FontFamily="Consolas,Menlo,monospace"/> <TextBlock Classes="path-mono" Text="{Binding}"/>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<TextBlock Text="{Binding VsCodeError}" Foreground="OrangeRed" <TextBlock Classes="meta" Text="{Binding VsCodeError}" Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding VsCodeError, Converter={x:Static ObjectConverters.IsNotNull}}" IsVisible="{Binding VsCodeError, Converter={x:Static ObjectConverters.IsNotNull}}"
TextWrapping="Wrap"/> 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}}" IsVisible="{Binding ActionError, Converter={x:Static ObjectConverters.IsNotNull}}"
TextWrapping="Wrap"/> 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> </StackPanel>
</Grid> </ctl:ModalShell>
</Border>
</Window> </Window>

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,24 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.State; using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Worktrees;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.External; namespace ClaudeDo.Worker.External;
public sealed record TaskListDto(string Id, string Name, string? WorkingDir); public sealed record TaskListDto(string Id, string Name, string? WorkingDir);
public sealed record DeleteTaskResult(bool Deleted, string Id);
public sealed record CancelTaskResult(bool Cancelled, string Id);
public sealed record StatusValueDto(string Status, string Meaning);
public sealed record TaskDto( public sealed record TaskDto(
string Id, string Id,
@@ -23,6 +32,23 @@ public sealed record TaskDto(
DateTime? StartedAt, DateTime? StartedAt,
DateTime? FinishedAt); DateTime? FinishedAt);
public sealed record WorktreeInfoDto(
string Path, string Branch, string HeadCommit, string BaseCommit,
int Ahead, int Behind, bool IsDirty);
public sealed record TaskDiffDto(
string Content, IReadOnlyList<string> Files, bool Truncated, int TotalBytes);
public sealed record MergeTaskResultDto(
bool Merged, string? MergeCommit, IReadOnlyList<string> Conflicts);
public sealed record WorktreeListItemDto(
string? TaskId, string Path, string Branch,
string HeadCommit, bool IsDirty, bool MergedIntoMain);
public sealed record CleanupWorktreeResult(
bool Removed, string WorktreePath, bool BranchDeleted);
[McpServerToolType] [McpServerToolType]
public sealed class ExternalMcpService public sealed class ExternalMcpService
{ {
@@ -31,19 +57,31 @@ public sealed class ExternalMcpService
private readonly QueueService _queue; private readonly QueueService _queue;
private readonly HubBroadcaster _broadcaster; private readonly HubBroadcaster _broadcaster;
private readonly ITaskStateService _state; private readonly ITaskStateService _state;
private readonly GitService _git;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly WorktreeMaintenanceService _maintenance;
private readonly TaskMergeService _merge;
public ExternalMcpService( public ExternalMcpService(
TaskRepository tasks, TaskRepository tasks,
ListRepository lists, ListRepository lists,
QueueService queue, QueueService queue,
HubBroadcaster broadcaster, HubBroadcaster broadcaster,
ITaskStateService state) ITaskStateService state,
GitService git,
IDbContextFactory<ClaudeDoDbContext> dbFactory,
WorktreeMaintenanceService maintenance,
TaskMergeService merge)
{ {
_tasks = tasks; _tasks = tasks;
_lists = lists; _lists = lists;
_queue = queue; _queue = queue;
_broadcaster = broadcaster; _broadcaster = broadcaster;
_state = state; _state = state;
_git = git;
_dbFactory = dbFactory;
_maintenance = maintenance;
_merge = merge;
} }
[McpServerTool, Description("List all task lists available in ClaudeDo.")] [McpServerTool, Description("List all task lists available in ClaudeDo.")]
@@ -53,7 +91,9 @@ public sealed class ExternalMcpService
return lists.Select(l => new TaskListDto(l.Id, l.Name, l.WorkingDir)).ToList(); return lists.Select(l => new TaskListDto(l.Id, l.Name, l.WorkingDir)).ToList();
} }
[McpServerTool, Description("List tasks in a given list. Optionally filter by creator (CreatedBy) and/or status.")] [McpServerTool, Description(
"List tasks in a given list. Optionally filter by creator (createdBy) and/or status. " +
"Valid status values: Idle, Queued, Running, Done, Failed, Cancelled.")]
public async Task<IReadOnlyList<TaskDto>> ListTasks( public async Task<IReadOnlyList<TaskDto>> ListTasks(
string listId, string listId,
string? createdBy, string? createdBy,
@@ -64,7 +104,8 @@ public sealed class ExternalMcpService
if (!string.IsNullOrWhiteSpace(status)) if (!string.IsNullOrWhiteSpace(status))
{ {
if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var parsed)) if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var parsed))
throw new InvalidOperationException($"Unknown status '{status}'."); throw new InvalidOperationException(
$"Unknown status '{status}'. Valid values: Idle, Queued, Running, Done, Failed, Cancelled.");
statusFilter = parsed; statusFilter = parsed;
} }
@@ -78,7 +119,10 @@ public sealed class ExternalMcpService
return query.Select(ToDto).ToList(); return query.Select(ToDto).ToList();
} }
[McpServerTool, Description("Get a single task by id, including its current status and result.")] [McpServerTool, Description(
"Get a single task by id, including its current status and result. " +
"Status lifecycle: Idle → Queued → Running → Done | Failed | Cancelled. " +
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution.")]
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken) public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
{ {
var task = await _tasks.GetByIdAsync(taskId, cancellationToken) var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
@@ -90,17 +134,15 @@ public sealed class ExternalMcpService
public async Task<TaskDto> AddTask( public async Task<TaskDto> AddTask(
string listId, string listId,
string title, string title,
string? description, string? description = null,
string createdBy, string? createdBy = null,
bool queueImmediately, bool queueImmediately = false,
CancellationToken cancellationToken) CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrWhiteSpace(listId)) if (string.IsNullOrWhiteSpace(listId))
throw new InvalidOperationException("listId is required."); throw new InvalidOperationException("listId is required.");
if (string.IsNullOrWhiteSpace(title)) if (string.IsNullOrWhiteSpace(title))
throw new InvalidOperationException("title is required."); throw new InvalidOperationException("title is required.");
if (string.IsNullOrWhiteSpace(createdBy))
throw new InvalidOperationException("createdBy is required.");
var list = await _lists.GetByIdAsync(listId, cancellationToken) var list = await _lists.GetByIdAsync(listId, cancellationToken)
?? throw new InvalidOperationException($"List {listId} not found."); ?? throw new InvalidOperationException($"List {listId} not found.");
@@ -114,13 +156,12 @@ public sealed class ExternalMcpService
Status = TaskStatus.Idle, Status = TaskStatus.Idle,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
CommitType = list.DefaultCommitType, CommitType = list.DefaultCommitType,
CreatedBy = createdBy, CreatedBy = createdBy.NullIfBlank() ?? "mcp",
}; };
await _tasks.AddAsync(entity, cancellationToken); await _tasks.AddAsync(entity, cancellationToken);
if (queueImmediately) if (queueImmediately)
{ {
// Routes through TaskStateService so the queue is woken automatically.
var enqueue = await _state.EnqueueAsync(entity.Id, cancellationToken); var enqueue = await _state.EnqueueAsync(entity.Id, cancellationToken);
if (!enqueue.Ok) if (!enqueue.Ok)
throw new InvalidOperationException(enqueue.Reason ?? "Cannot enqueue task."); throw new InvalidOperationException(enqueue.Reason ?? "Cannot enqueue task.");
@@ -154,14 +195,19 @@ public sealed class ExternalMcpService
return ToDto(reload); return ToDto(reload);
} }
[McpServerTool, Description("Update a task's status. Only 'Idle' and 'Queued' are permitted — use RunTaskNow or CancelTask for execution control.")] [McpServerTool, Description(
"Update a task's status. Only 'Idle' and 'Queued' are permitted externally — " +
"use run_task_now or cancel_task for execution control. " +
"Settable: Idle (reset to editable), Queued (enqueue for execution). " +
"Full lifecycle: Idle → Queued → Running → Done | Failed | Cancelled.")]
public async Task<TaskDto> UpdateTaskStatus( public async Task<TaskDto> UpdateTaskStatus(
string taskId, string taskId,
string status, string status,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var target)) if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var target))
throw new InvalidOperationException($"Unknown status '{status}'."); throw new InvalidOperationException(
$"Unknown status '{status}'. Valid values: Idle, Queued, Running, Done, Failed, Cancelled.");
var task = await _tasks.GetByIdAsync(taskId, cancellationToken) var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found."); ?? throw new InvalidOperationException($"Task {taskId} not found.");
@@ -181,7 +227,7 @@ public sealed class ExternalMcpService
default: default:
throw new InvalidOperationException( throw new InvalidOperationException(
$"Status '{target}' is not settable externally. Use RunTaskNow or CancelTask."); $"Status '{target}' is not settable externally. Use run_task_now or cancel_task.");
} }
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!; var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
@@ -206,16 +252,16 @@ public sealed class ExternalMcpService
await _broadcaster.TaskUpdated(taskId); await _broadcaster.TaskUpdated(taskId);
} }
[McpServerTool, Description("Cancel a running task. Returns true if the task was running and cancellation was requested.")] [McpServerTool, Description("Cancel a running task. Returns { cancelled: true, id } if the task was running and cancellation was requested; cancelled is false if the task was not running.")]
public async Task<bool> CancelTask(string taskId, CancellationToken cancellationToken) public async Task<CancelTaskResult> CancelTask(string taskId, CancellationToken cancellationToken)
{ {
var cancelled = _queue.CancelTask(taskId); var cancelled = _queue.CancelTask(taskId);
if (cancelled) await _broadcaster.TaskUpdated(taskId); if (cancelled) await _broadcaster.TaskUpdated(taskId);
return cancelled; return new CancelTaskResult(cancelled, taskId);
} }
[McpServerTool, Description("Delete a task. Refuses if the task is currently Running — cancel it first.")] [McpServerTool, Description("Delete a task. Returns { deleted: true, id } on success. Throws if the task is not found or is currently Running — cancel it first.")]
public async Task DeleteTask(string taskId, CancellationToken cancellationToken) public async Task<DeleteTaskResult> DeleteTask(string taskId, CancellationToken cancellationToken)
{ {
var task = await _tasks.GetByIdAsync(taskId, cancellationToken) var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found."); ?? throw new InvalidOperationException($"Task {taskId} not found.");
@@ -224,6 +270,257 @@ public sealed class ExternalMcpService
await _tasks.DeleteAsync(taskId, cancellationToken); await _tasks.DeleteAsync(taskId, cancellationToken);
await _broadcaster.TaskUpdated(taskId); await _broadcaster.TaskUpdated(taskId);
return new DeleteTaskResult(true, taskId);
}
// ── Status reference ─────────────────────────────────────────────────────
[McpServerTool, Description("Returns all valid task status values and their meanings. Use before filtering by status or interpreting task state.")]
public Task<IReadOnlyList<StatusValueDto>> GetTaskStatusValues() =>
Task.FromResult<IReadOnlyList<StatusValueDto>>([
new("Idle", "Not yet queued; task is editable and will not run until enqueued."),
new("Queued", "Waiting for an agent execution slot. Tasks with a blocker (BlockedByTaskId) are skipped by the queue picker until their predecessor finishes."),
new("Running", "Agent is actively executing the task; cannot be edited or deleted until cancelled."),
new("Done", "Completed successfully; result text is available in the result field. Can be reset to Idle for re-execution."),
new("Failed", "Execution ended with an error; task can be reset to Idle or re-queued directly."),
new("Cancelled", "Cancelled by the user; task can be reset to Idle or re-queued directly."),
]);
// ── Worktree / git tools ──────────────────────────────────────────────────
[McpServerTool, Description(
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
"isDirty (has uncommitted changes in the worktree directory). " +
"Throws if the task or its worktree does not exist.")]
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
{
var (_, _, wt) = await LoadWorktreeContextAsync(taskId, cancellationToken);
var headCommit = !string.IsNullOrWhiteSpace(wt.HeadCommit)
? wt.HeadCommit
: await TryRunGitAsync(wt.Path, ["rev-parse", "HEAD"], cancellationToken) ?? wt.BaseCommit;
var isDirty = Directory.Exists(wt.Path) && await _git.HasChangesAsync(wt.Path, cancellationToken);
var ahead = await GitRevListCountAsync(wt.Path, $"{wt.BaseCommit}..HEAD", cancellationToken);
var behind = await GitRevListCountAsync(wt.Path, "HEAD..main", cancellationToken);
return new WorktreeInfoDto(wt.Path, wt.BranchName, headCommit!, wt.BaseCommit, ahead, behind, isDirty);
}
[McpServerTool, Description(
"Get the diff for a task's worktree relative to its base commit. " +
"stat=false (default): returns the full unified diff, capped at 200 KB (truncated=true when larger). " +
"stat=true: returns a --stat summary (changed files with insertion/deletion counts). " +
"files always lists the changed file paths regardless of stat mode. " +
"totalBytes is the uncapped diff size (useful when truncated=true). " +
"Throws if the task has no worktree or the worktree directory is missing from disk.")]
public async Task<TaskDiffDto> GetTaskDiff(
string taskId, bool stat = false, CancellationToken cancellationToken = default)
{
var (_, _, wt) = await LoadWorktreeContextAsync(taskId, cancellationToken);
if (!Directory.Exists(wt.Path))
throw new InvalidOperationException($"Worktree directory does not exist on disk: {wt.Path}");
const int maxBytes = 200 * 1024;
if (stat)
{
var diffStat = await _git.DiffStatAsync(wt.Path, wt.BaseCommit, "HEAD", cancellationToken);
return new TaskDiffDto(diffStat, ParseDiffStatFileNames(diffStat), false, diffStat.Length);
}
var diff = await _git.GetBranchDiffAsync(wt.Path, wt.BaseCommit, cancellationToken);
var files = ParseDiffFileNames(diff);
if (diff.Length <= maxBytes)
return new TaskDiffDto(diff, files, false, diff.Length);
return new TaskDiffDto(diff[..maxBytes], files, true, diff.Length);
}
[McpServerTool, Description(
"Merge a task's worktree branch into targetBranch (default: main). " +
"noFf=true (default): always creates a merge commit (--no-ff). " +
"dryRun=true: validates preconditions only, does not perform the merge; merged=false in the result means 'not actually merged'. " +
"Refuses if task status is not Done (status values: Idle, Queued, Running, Done, Failed, Cancelled). " +
"On success: merged=true, mergeCommit contains the new merge commit SHA. " +
"On conflict: the merge is cleanly aborted (no half-merged state left); merged=false and conflicts lists the affected files.")]
public async Task<MergeTaskResultDto> MergeTask(
string taskId,
string targetBranch = "main",
bool noFf = true,
bool dryRun = false,
CancellationToken cancellationToken = default)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
if (task.Status != TaskStatus.Done)
throw new InvalidOperationException(
$"Task must be Done to merge (current status: {task.Status}). " +
"Valid statuses for merge: Done.");
var list = await _lists.GetByIdAsync(task.ListId, cancellationToken);
if (dryRun)
{
using var ctx = _dbFactory.CreateDbContext();
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} has no worktree.");
if (wt.State != WorktreeState.Active)
throw new InvalidOperationException(
$"Worktree state must be Active to merge (current: {wt.State}).");
return new MergeTaskResultDto(false, null, []);
}
var commitMessage = $"Merge task branch for: {task.Title}";
var result = await _merge.MergeAsync(
taskId, targetBranch, removeWorktree: false, commitMessage, cancellationToken);
if (result.Status == TaskMergeService.StatusMerged)
{
string? mergeCommit = null;
try
{
if (!string.IsNullOrWhiteSpace(list?.WorkingDir) && Directory.Exists(list.WorkingDir))
mergeCommit = await _git.RevParseHeadAsync(list.WorkingDir, cancellationToken);
}
catch { /* mergeCommit is optional */ }
return new MergeTaskResultDto(true, mergeCommit, []);
}
if (result.Status == TaskMergeService.StatusConflict)
return new MergeTaskResultDto(false, null, result.ConflictFiles);
throw new InvalidOperationException(result.ErrorMessage ?? $"Merge blocked: {result.Status}");
}
[McpServerTool, Description(
"List all ClaudeDo-tracked worktrees. " +
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +
"isDirty (has uncommitted changes), mergedIntoMain (worktree state is Merged). " +
"Only worktrees recorded in the ClaudeDo database are returned.")]
public async Task<IReadOnlyList<WorktreeListItemDto>> ListWorktrees(CancellationToken cancellationToken)
{
var rows = await _maintenance.GetOverviewAsync(null, cancellationToken);
var results = await Task.WhenAll(rows.Select(async row =>
{
var isDirty = row.PathExistsOnDisk && await TryGetIsDirtyAsync(row.Path, cancellationToken);
var headCommit = row.PathExistsOnDisk
? (await TryRunGitAsync(row.Path, ["rev-parse", "HEAD"], cancellationToken) ?? "")
: "";
return new WorktreeListItemDto(
row.TaskId, row.Path, row.BranchName, headCommit,
isDirty, row.State == WorktreeState.Merged);
}));
return results;
}
[McpServerTool, Description(
"Remove a task's worktree directory and delete its git branch. " +
"force=false (default): refuses if the worktree has uncommitted changes or the task is Running. " +
"force=true: removes even a dirty worktree (uncommitted changes are lost); task must not be Running. " +
"Returns removed=true on success; branchDeleted reflects whether the branch was also removed.")]
public async Task<CleanupWorktreeResult> CleanupTaskWorktree(
string taskId, bool force = false, CancellationToken cancellationToken = default)
{
using var ctx = _dbFactory.CreateDbContext();
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} has no worktree.");
if (task.Status == TaskStatus.Running)
throw new InvalidOperationException("Cannot remove worktree of a running task.");
if (!force && Directory.Exists(wt.Path))
{
var isDirty = await _git.HasChangesAsync(wt.Path, cancellationToken);
if (isDirty)
throw new InvalidOperationException(
"Worktree has uncommitted changes. Use force=true to remove anyway (changes will be lost).");
}
var path = wt.Path;
var result = await _maintenance.ForceRemoveAsync(taskId, cancellationToken);
return new CleanupWorktreeResult(result.Removed, path, result.Removed);
}
// ── Private helpers ───────────────────────────────────────────────────────
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity Wt)> LoadWorktreeContextAsync(
string taskId, CancellationToken ct)
{
using var ctx = _dbFactory.CreateDbContext();
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
?? throw new InvalidOperationException($"Task {taskId} not found.");
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
?? throw new InvalidOperationException("List not found.");
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct)
?? throw new InvalidOperationException($"Task {taskId} has no worktree.");
return (task, list, wt);
}
private async Task<bool> TryGetIsDirtyAsync(string path, CancellationToken ct)
{
try { return await _git.HasChangesAsync(path, ct); }
catch { return false; }
}
// Minimal git runner for operations not covered by GitService (rev-list --count, rev-parse from worktree).
private static async Task<string?> TryRunGitAsync(string dir, string[] args, CancellationToken ct)
{
try
{
var psi = new ProcessStartInfo("git")
{
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
psi.ArgumentList.Add("-C");
psi.ArgumentList.Add(dir);
foreach (var a in args) psi.ArgumentList.Add(a);
using var proc = Process.Start(psi)!;
await using var _ = ct.Register(() => { try { proc.Kill(entireProcessTree: true); } catch { } });
var stdout = await proc.StandardOutput.ReadToEndAsync();
await proc.WaitForExitAsync(CancellationToken.None);
ct.ThrowIfCancellationRequested();
return proc.ExitCode == 0 ? stdout.Trim() : null;
}
catch (OperationCanceledException) { throw; }
catch { return null; }
}
private static async Task<int> GitRevListCountAsync(string dir, string range, CancellationToken ct)
{
var result = await TryRunGitAsync(dir, ["rev-list", "--count", range], ct);
return int.TryParse(result, out var n) ? n : 0;
}
private static IReadOnlyList<string> ParseDiffFileNames(string diff)
{
var files = new List<string>();
foreach (var line in diff.Split('\n'))
{
var s = line.TrimEnd('\r');
if (s.StartsWith("+++ b/", StringComparison.Ordinal))
files.Add(s[6..]);
}
return files;
}
private static IReadOnlyList<string> ParseDiffStatFileNames(string stat)
{
var files = new List<string>();
foreach (var line in stat.Split('\n'))
{
var idx = line.IndexOf('|');
if (idx > 0) files.Add(line[..idx].Trim());
}
return files;
} }
private static TaskDto ToDto(TaskEntity t) => new( private static TaskDto ToDto(TaskEntity t) => new(

View File

@@ -11,6 +11,12 @@ public sealed record RunDto(
int? ExitCode, int? TurnCount, int? TokensIn, int? TokensOut, int? ExitCode, int? TurnCount, int? TokensIn, int? TokensOut,
DateTime? StartedAt, DateTime? FinishedAt); DateTime? StartedAt, DateTime? FinishedAt);
public sealed record TaskLogResult(
bool Available,
IReadOnlyList<string> Entries,
int TotalLines,
bool Truncated);
[McpServerToolType] [McpServerToolType]
public sealed class RunHistoryMcpTools public sealed class RunHistoryMcpTools
{ {
@@ -33,26 +39,68 @@ public sealed class RunHistoryMcpTools
return ToDto(run); return ToDto(run);
} }
private const int MaxLogBytes = 256 * 1024; [McpServerTool, Description(
"Fetch log entries from a task's latest run. " +
[McpServerTool, Description("Fetch the raw log output of a task's latest run. Throws if no log is available.")] "Returns { available, entries, totalLines, truncated }. " +
public async Task<string> GetTaskLog(string taskId, CancellationToken cancellationToken) "available=false means no log exists yet (task is queued or just started — not an error). " +
"entries are the individual lines (NDJSON messages) from Claude's streaming output. " +
"Default: returns the last 50 entries (tail=50). " +
"tail: override the number of trailing entries to return. " +
"offset+limit: return entries starting at position offset (0-based); overrides tail when provided. " +
"truncated=true when fewer entries are returned than totalLines.")]
public async Task<TaskLogResult> GetTaskLog(
string taskId,
int? tail = null,
int? offset = null,
int? limit = null,
CancellationToken cancellationToken = default)
{ {
var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken) var run = await _runs.GetLatestByTaskIdAsync(taskId, cancellationToken);
?? throw new InvalidOperationException($"No runs found for task {taskId}."); if (run is null || string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath))
if (string.IsNullOrWhiteSpace(run.LogPath) || !File.Exists(run.LogPath)) return new TaskLogResult(false, [], 0, false);
throw new InvalidOperationException("No log available for the latest run.");
var totalBytes = new FileInfo(run.LogPath).Length; string allText;
if (totalBytes <= MaxLogBytes) try
return await File.ReadAllTextAsync(run.LogPath, cancellationToken); {
await using var fs = new FileStream(
run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(fs);
allText = await reader.ReadToEndAsync(cancellationToken);
}
catch (IOException)
{
return new TaskLogResult(false, [], 0, false);
}
var buffer = new byte[MaxLogBytes]; var lines = allText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
await using var fs = new FileStream(run.LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var totalLines = lines.Length;
fs.Seek(totalBytes - MaxLogBytes, SeekOrigin.Begin);
var read = await fs.ReadAsync(buffer, cancellationToken); IReadOnlyList<string> entries;
var tail = System.Text.Encoding.UTF8.GetString(buffer, 0, read); bool truncated;
return $"[truncated: showing last {MaxLogBytes} of {totalBytes} bytes]\n{tail}";
if (offset.HasValue || limit.HasValue)
{
var start = Math.Max(0, offset ?? 0);
var count = limit.HasValue ? Math.Min(limit.Value, totalLines - start) : totalLines - start;
entries = lines.Skip(start).Take(count).ToArray();
truncated = start > 0 || (start + count) < totalLines;
}
else
{
var take = tail ?? 50;
if (totalLines <= take)
{
entries = lines;
truncated = false;
}
else
{
entries = lines[^take..];
truncated = true;
}
}
return new TaskLogResult(true, entries, totalLines, truncated);
} }
private static RunDto ToDto(TaskRunEntity r) => new( private static RunDto ToDto(TaskRunEntity r) => new(

View File

@@ -22,6 +22,7 @@ public record AppSettingsDto(
string DefaultModel, string DefaultModel,
int DefaultMaxTurns, int DefaultMaxTurns,
string DefaultPermissionMode, string DefaultPermissionMode,
int MaxParallelExecutions,
string WorktreeStrategy, string WorktreeStrategy,
string? CentralWorktreeRoot, string? CentralWorktreeRoot,
bool WorktreeAutoCleanupEnabled, bool WorktreeAutoCleanupEnabled,
@@ -202,6 +203,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
row.DefaultModel, row.DefaultModel,
row.DefaultMaxTurns, row.DefaultMaxTurns,
row.DefaultPermissionMode, row.DefaultPermissionMode,
row.MaxParallelExecutions,
row.WorktreeStrategy, row.WorktreeStrategy,
row.CentralWorktreeRoot, row.CentralWorktreeRoot,
row.WorktreeAutoCleanupEnabled, row.WorktreeAutoCleanupEnabled,
@@ -219,6 +221,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
DefaultModel = dto.DefaultModel ?? ModelRegistry.DefaultAlias, DefaultModel = dto.DefaultModel ?? ModelRegistry.DefaultAlias,
DefaultMaxTurns = dto.DefaultMaxTurns, DefaultMaxTurns = dto.DefaultMaxTurns,
DefaultPermissionMode = dto.DefaultPermissionMode ?? PermissionModeRegistry.DefaultMode, DefaultPermissionMode = dto.DefaultPermissionMode ?? PermissionModeRegistry.DefaultMode,
MaxParallelExecutions = dto.MaxParallelExecutions,
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling", WorktreeStrategy = dto.WorktreeStrategy ?? "sibling",
CentralWorktreeRoot = dto.CentralWorktreeRoot, CentralWorktreeRoot = dto.CentralWorktreeRoot,
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled, WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled,

View File

@@ -204,6 +204,9 @@ if (cfg.ExternalMcpPort > 0)
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeManager>()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeManager>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<AgentFileService>()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<AgentFileService>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskResetService>()); externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskResetService>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<GitService>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<WorktreeMaintenanceService>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<TaskMergeService>());
externalBuilder.Services.AddScoped<ExternalMcpService>(); externalBuilder.Services.AddScoped<ExternalMcpService>();
externalBuilder.Services.AddScoped<ListMcpTools>(); externalBuilder.Services.AddScoped<ListMcpTools>();
externalBuilder.Services.AddScoped<ConfigMcpTools>(); externalBuilder.Services.AddScoped<ConfigMcpTools>();

View File

@@ -18,7 +18,7 @@ public sealed class QueueService : BackgroundService
private readonly OverrideSlotService _override; private readonly OverrideSlotService _override;
private readonly object _lock = new(); private readonly object _lock = new();
private volatile QueueSlotState? _queueSlot; private readonly Dictionary<string, QueueSlotState> _queueSlots = new();
public QueueService( public QueueService(
IDbContextFactory<ClaudeDoDbContext> dbFactory, IDbContextFactory<ClaudeDoDbContext> dbFactory,
@@ -41,8 +41,11 @@ public sealed class QueueService : BackgroundService
public IReadOnlyList<(string slot, string taskId, DateTime startedAt)> GetActive() public IReadOnlyList<(string slot, string taskId, DateTime startedAt)> GetActive()
{ {
var list = new List<(string, string, DateTime)>(); var list = new List<(string, string, DateTime)>();
var q = _queueSlot; lock (_lock)
if (q is not null) list.Add(("queue", q.TaskId, q.StartedAt)); {
foreach (var slot in _queueSlots.Values)
list.Add(("queue", slot.TaskId, slot.StartedAt));
}
var o = _override.CurrentSlot; var o = _override.CurrentSlot;
if (o is not null) list.Add(("override", o.TaskId, o.StartedAt)); if (o is not null) list.Add(("override", o.TaskId, o.StartedAt));
return list; return list;
@@ -64,7 +67,7 @@ public sealed class QueueService : BackgroundService
{ {
lock (_lock) lock (_lock)
{ {
if (_queueSlot?.TaskId == taskId) if (_queueSlots.ContainsKey(taskId))
throw new InvalidOperationException("task is already running in queue slot"); throw new InvalidOperationException("task is already running in queue slot");
} }
} }
@@ -75,9 +78,9 @@ public sealed class QueueService : BackgroundService
lock (_lock) lock (_lock)
{ {
if (_queueSlot is not null && _queueSlot.TaskId == taskId) if (_queueSlots.TryGetValue(taskId, out var slot))
{ {
_queueSlot.Cts.Cancel(); slot.Cts.Cancel();
return true; return true;
} }
} }
@@ -100,28 +103,35 @@ public sealed class QueueService : BackgroundService
await Task.WhenAny(wakeTask, timerTask); await Task.WhenAny(wakeTask, timerTask);
if (_queueSlot is not null) continue; var maxParallel = await GetMaxParallelAsync(stoppingToken);
// Fill as many free slots as the limit allows.
while (!stoppingToken.IsCancellationRequested)
{
lock (_lock)
{
if (_queueSlots.Count >= maxParallel) break;
}
var task = await _picker.ClaimNextAsync(DateTime.UtcNow, stoppingToken); var task = await _picker.ClaimNextAsync(DateTime.UtcNow, stoppingToken);
if (task is null) continue; if (task is null) break;
lock (_lock) lock (_lock)
{ {
if (_queueSlot is not null) continue;
var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
_queueSlot = new QueueSlotState { TaskId = task.Id, StartedAt = DateTime.UtcNow, Cts = cts }; _queueSlots[task.Id] = new QueueSlotState { TaskId = task.Id, StartedAt = DateTime.UtcNow, Cts = cts };
_ = RunInSlotAsync(task.Id, cts.Token).ContinueWith(t => _ = RunInSlotAsync(task.Id, cts.Token).ContinueWith(t =>
{ {
if (t.IsFaulted) if (t.IsFaulted)
_logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId} in queue slot", task.Id); _logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId} in queue slot", task.Id);
lock (_lock) { _queueSlot = null; } lock (_lock) { _queueSlots.Remove(task.Id); }
cts.Dispose(); cts.Dispose();
_waker.Wake(); // Check for next task immediately. _waker.Wake(); // Check for next task immediately.
}, TaskScheduler.Default); }, TaskScheduler.Default);
} }
} }
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{ {
break; break;
@@ -135,6 +145,21 @@ public sealed class QueueService : BackgroundService
_logger.LogInformation("QueueService stopping"); _logger.LogInformation("QueueService stopping");
} }
private async Task<int> GetMaxParallelAsync(CancellationToken ct)
{
try
{
using var context = _dbFactory.CreateDbContext();
var settings = await new AppSettingsRepository(context).GetAsync(ct);
return Math.Max(1, settings.MaxParallelExecutions);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read max parallel executions; defaulting to 1");
return 1;
}
}
private async Task RunInSlotAsync(string taskId, CancellationToken ct) private async Task RunInSlotAsync(string taskId, CancellationToken ct)
{ {
try try

Some files were not shown because too many files have changed in this diff Show More