Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83ea429b8a | ||
|
|
032debc780 | ||
|
|
f822c50102 | ||
|
|
7dcbbf484e | ||
|
|
5872666f31 | ||
|
|
cf9dd1cc94 | ||
|
|
677a4c1853 | ||
|
|
338fc3903d | ||
|
|
57326f1d3b | ||
|
|
8103006e26 | ||
|
|
5115cfc288 | ||
|
|
1e88fefbda | ||
|
|
7661129202 | ||
|
|
7cbd4e66ae | ||
|
|
6e2158d157 | ||
|
|
d4cd202460 | ||
|
|
519ea5a8e0 | ||
|
|
1aaa40b894 | ||
|
|
3e7126b3f2 | ||
|
|
d2ca7fb500 | ||
|
|
10e561f336 | ||
|
|
32c019bd5d | ||
|
|
65db1cdefa | ||
|
|
5ba0b63e03 | ||
|
|
42c70fb28c | ||
|
|
c9ba1e2645 | ||
|
|
394febadeb | ||
|
|
1ee21b560d | ||
|
|
8f8c2a65b2 | ||
|
|
6c5acd09b9 | ||
|
|
a7b88098ab | ||
|
|
ee84a75bd7 | ||
|
|
1cc247a590 | ||
|
|
c871f35513 | ||
|
|
3972ce50a6 | ||
|
|
8ed6e08710 | ||
|
|
194ce58a72 | ||
|
|
b38b0857dd | ||
|
|
334cf1e1d2 | ||
|
|
b126a21c57 | ||
|
|
b1efcdce87 | ||
|
|
e926f4db81 | ||
|
|
20d17c6887 | ||
|
|
bedd2defbb | ||
|
|
8d7ba1e314 | ||
|
|
48de816051 | ||
|
|
cc3013a76e | ||
|
|
eee87f086c | ||
|
|
9804ecefff | ||
|
|
7d29ec0725 | ||
|
|
524836ffe8 | ||
|
|
47e2734357 | ||
|
|
37da35b903 | ||
|
|
6a2a40cd46 | ||
|
|
01cd47ffec | ||
|
|
3a07b319f9 | ||
|
|
c07c1f70a8 | ||
|
|
52c2186999 | ||
|
|
47edab5907 | ||
|
|
87de53e052 | ||
|
|
c82ea2eea1 | ||
|
|
7d636c60dd | ||
|
|
0b6792620c | ||
|
|
fd50a4fb7c | ||
|
|
342a061d94 | ||
|
|
63d8b5c28d | ||
|
|
ab56644ddc | ||
|
|
71050e2634 | ||
|
|
ef7645c6b1 | ||
|
|
92ce7a4a77 | ||
|
|
4dc4fe27e3 | ||
|
|
0ee30bee03 | ||
|
|
b9e0721875 | ||
|
|
92eb654f9b | ||
|
|
724814f770 | ||
|
|
b5464fc533 | ||
|
|
44cdad386c | ||
|
|
58f8b11dbb | ||
|
|
e653677487 | ||
|
|
994e94c2af | ||
|
|
db447f36da | ||
|
|
df2fcd8def | ||
|
|
17ef99bc9b | ||
|
|
c4425d6499 | ||
|
|
be6ccb2c17 | ||
|
|
57d433276e | ||
|
|
785ebe55e4 | ||
|
|
e1807fd53b | ||
|
|
149e2adadb | ||
|
|
d569313598 | ||
|
|
0a3c25840f | ||
|
|
7d6cb2bd3e | ||
|
|
7c8a9dd61b | ||
|
|
3fbbd7ab93 | ||
|
|
24f999facd | ||
|
|
3a648b7d77 | ||
|
|
fde9615b34 | ||
|
|
c93a20f20c | ||
|
|
1466d0fbab | ||
|
|
6c8de4aef3 | ||
|
|
7f9f0ca128 | ||
|
|
edd2774d86 | ||
|
|
62fc5aaa5a | ||
|
|
023e136c53 | ||
|
|
4877802bcd | ||
|
|
40eb979924 | ||
|
|
e3bacc3143 | ||
|
|
cc823ec4f6 | ||
|
|
ec10b06848 | ||
|
|
81cf94187f | ||
|
|
538b4ede09 | ||
|
|
95918414a0 | ||
|
|
9349386675 | ||
|
|
083e1f3948 | ||
|
|
ecbc73495c | ||
|
|
327ae2b69c | ||
|
|
5ba0c09d8f | ||
|
|
78d4e1a46b | ||
|
|
c7d64e9c9b | ||
|
|
7517f2a9b3 | ||
|
|
f4f7c81059 | ||
|
|
2a3ab5504a | ||
|
|
962f68c92b | ||
|
|
d12a888683 | ||
|
|
5c3ec4810e | ||
|
|
49222a92a4 | ||
|
|
109a35c505 | ||
|
|
34b17537fc | ||
|
|
2aaaa23912 | ||
|
|
624ec7a668 | ||
|
|
3e9ea3ad58 | ||
|
|
ef285b21fd | ||
|
|
2612831a5e | ||
|
|
5e27486b18 | ||
|
|
04044bd115 | ||
|
|
798d100636 | ||
|
|
e8f7e3a47a | ||
|
|
8a7275a75f | ||
|
|
f4dd67d595 | ||
|
|
0226c98076 | ||
|
|
b9b3053051 | ||
|
|
671c886c75 | ||
|
|
ffff1ee183 | ||
|
|
da6a70aec1 | ||
|
|
85d0f9dcec | ||
|
|
ad2acddc9a | ||
|
|
efd7cc9b0a | ||
|
|
75a6e0efc4 | ||
|
|
9efc5c90f4 | ||
|
|
6ca8cac79f | ||
|
|
9a1fa3dbaf | ||
|
|
9b4d3431b0 | ||
|
|
a1ba3b6ebb | ||
|
|
9b041ba791 | ||
|
|
39fc594b98 | ||
|
|
b353ed6cfd | ||
|
|
416e47ecef | ||
|
|
e8b5e97a9b | ||
|
|
d19ef5403f | ||
|
|
ded068c564 | ||
|
|
9556e0e848 | ||
|
|
9d39a8fc69 | ||
|
|
b536b6fb66 | ||
|
|
1b80bb0a2d | ||
|
|
4394623bb0 | ||
|
|
f0b0582517 | ||
|
|
07de897147 | ||
|
|
21012283a9 | ||
|
|
8241bf8d41 | ||
|
|
255705d8bf | ||
|
|
3dfd75fba8 | ||
|
|
26c03a5a5f | ||
|
|
3211bfc0f9 | ||
|
|
79ce7afe46 | ||
|
|
0ad93f48b5 | ||
|
|
fee69998f8 | ||
|
|
451afc80f8 | ||
|
|
4b2667062f | ||
|
|
305eb6ee8b | ||
|
|
15bef2a2e3 | ||
|
|
d7ebafd556 | ||
|
|
14e4c086e2 | ||
|
|
0f2d202b01 | ||
|
|
2a4633fdd8 | ||
|
|
3d668da65c | ||
|
|
f18e03354a | ||
|
|
a1986dd485 | ||
|
|
110364aa6c | ||
|
|
0b86ccd74b | ||
|
|
d6891b8bd4 | ||
|
|
377409e633 | ||
|
|
941c8b98cc | ||
|
|
2452e39345 | ||
|
|
816f247d90 | ||
|
|
ad58129194 | ||
|
|
85c7e650c9 | ||
|
|
c412a84fdf | ||
|
|
d8194ad57e | ||
|
|
d9a4627a1f | ||
|
|
2b06ab0ab4 | ||
|
|
bb62740ac8 | ||
|
|
25922a2768 | ||
|
|
3feb08d9d9 | ||
|
|
9ab48d7094 | ||
|
|
0513265c49 | ||
|
|
d28c63d2df | ||
|
|
5f740c05d8 | ||
|
|
1245e75902 | ||
|
|
d91ad2d635 | ||
|
|
17235a6cde | ||
|
|
f33838d028 | ||
|
|
1cfd96c15f | ||
|
|
7c3c061428 | ||
|
|
b4c58087d2 | ||
|
|
4626481359 | ||
|
|
dea2b7db8b | ||
|
|
54cdaf89d5 | ||
|
|
dbaefe92c6 | ||
|
|
62b245aaea | ||
|
|
4e5057d3f6 | ||
|
|
1bf08eca27 | ||
|
|
eb88dc130c | ||
|
|
140ae2fda1 | ||
|
|
865e12c0de | ||
|
|
914fa5aa9f |
+284
@@ -1,5 +1,289 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.6.0 — 2026-08-04
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- unbreak the update path and cache the download (63d8b5c)
|
||||||
|
- clear task selection when switching lists (ab56644)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.5.0 (71050e2)
|
||||||
|
|
||||||
|
## v2.5.0 — 2026-07-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- move a task to another list via drag & drop (92ce7a4)
|
||||||
|
- sidebar shows queued+running, no auto-monitor seeding (44cdad3)
|
||||||
|
- expose maxParallelExecutions in get_app_settings (e653677)
|
||||||
|
- accent color presets in Settings → General (149e2ad)
|
||||||
|
- allow Done via update_task_status with worktree guard (d569313)
|
||||||
|
- add Let Claude handle it broom button to tasks header (7d6cb2b)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- stop swallowing mutating hub call failures (b5464fc)
|
||||||
|
- pin LogRingBuffer clock in Does_not_throw_when_detached (58f8b11)
|
||||||
|
- propagate HubException from ApproveReviewAsync so blocked merges surface errors (e1807fd)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.4.0 (3fbbd7a)
|
||||||
|
|
||||||
|
## v2.4.0 — 2026-07-27
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- mark tasks and lists as manual (3a648b7)
|
||||||
|
- per-model effort and turn presets (fde9615)
|
||||||
|
- Interactive chip for tasks with an open ConPTY session (c93a20f)
|
||||||
|
- spinners for ConPTY session start and task refine (1466d0f)
|
||||||
|
- five-phase list-handler prompt with dedupe and enhance (e3bacc3)
|
||||||
|
- allow update_task_status to set Cancelled (cc823ec)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- bind search focus to Ctrl+K instead of OemQuestion (7f9f0ca)
|
||||||
|
- persist title edits from the detail pane (edd2774)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- make the list-handler launch spec single-list and single-repo (4877802)
|
||||||
|
- scope "Let Claude handle it" to a single list (40eb979)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- record the manual-task, effort-preset and chip/spinner changes (24f999f)
|
||||||
|
- document the list-handler selection modal (62fc5aa)
|
||||||
|
- describe the list-scoped five-phase handler (023e136)
|
||||||
|
- implementation plan for the per-list task handler (ec10b06)
|
||||||
|
- per-list task handler with read/dedupe/enhance/run/merge (81cf941)
|
||||||
|
- update for v2.3.1 (538b4ed)
|
||||||
|
|
||||||
|
## v2.3.1 — 2026-07-24
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- retry stashing app/worker through transient file locks on update (9591841)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.3.0 (9349386)
|
||||||
|
|
||||||
|
## v2.3.0 — 2026-07-24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- open merge-helper ConPTY tile from selection (083e1f3)
|
||||||
|
- add "Let Claude handle it" entry points (ecbc734)
|
||||||
|
- add merge-helper task selection dialog (327ae2b)
|
||||||
|
- expose merge-helper launch spec over hub + client (5ba0c09)
|
||||||
|
- build merge-helper interactive launch spec (78d4e1a)
|
||||||
|
- add merge-helper prompt templates (c7d64e9)
|
||||||
|
- add continue_merge and abort_merge MCP tools (7517f2a)
|
||||||
|
- let review_task/merge_task leave conflicts in tree via MCP (f4f7c81)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- helper handles all merges; manual conflict fallback (2a3ab55)
|
||||||
|
- spec + implementation plan (962f68c)
|
||||||
|
- update for v2.2.0 (d12a888)
|
||||||
|
|
||||||
|
## v2.2.0 — 2026-07-24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- submit interactive (ConPTY) work for review (109a35c)
|
||||||
|
- gate Approve & Merge behind opening the diff (2aaaa23)
|
||||||
|
- run interactive planning sessions via embedded ConPTY (ef285b2)
|
||||||
|
- AskUser question banner in the detail island (798d100)
|
||||||
|
- conflict resolver shows why Continue is disabled (da6a70a)
|
||||||
|
- session-skills empty-state + neutral subtask terminology (85d0f9d)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- use default permission mode so MCP planning tools don't prompt (624ec7a)
|
||||||
|
- hide misleading Idle chip on planning parents (e8f7e3a)
|
||||||
|
- show structured-output summary instead of raw JSON in OUTCOME (8a7275a)
|
||||||
|
- live-refresh child rows on parent planning transitions (f4dd67d)
|
||||||
|
- restore turn/token counts on task reload (0226c98)
|
||||||
|
- clearer rename display in diff viewer (b9b3053)
|
||||||
|
- diagnostic error surfacing on attachment drop (671c886)
|
||||||
|
- surface resume-planning-session failures (ffff1ee)
|
||||||
|
- render Plus and agent-settings gear icons (ad2acdd)
|
||||||
|
- kill cancelled runs' processes and make MCP approve actually merge (fee6999)
|
||||||
|
- make external MCP filter params optional, surface tool errors (d7ebafd)
|
||||||
|
- merge preflights ignore untracked files in target working tree (14e4c08)
|
||||||
|
- UnifiedDiffParser mishandles paths with spaces and git-quoted paths (0f2d202)
|
||||||
|
- validate conflict markers before staging in ContinueMergeAsync (377409e)
|
||||||
|
- cascade cancel of a WaitingForChildren parent to its non-terminal children (941c8b9)
|
||||||
|
- advance parent when the last non-terminal child is deleted (2452e39)
|
||||||
|
- keep planning-chain cascade moving past an Idle middle link (816f247)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- spec for ConPTY planning sessions (2612831)
|
||||||
|
- record session progress (A/B done, C#9+#11, D#13+#14; C#10/#12 + group E deferred) (04044bd)
|
||||||
|
- add fix-plan for fresh session (findings grouped by fixability); defer §10, mark §11 OK per Mika (efd7cc9)
|
||||||
|
- §3 UnfinishedPlanning modal (Finalize/Discard PASS, Resume BUG); generalize child-row live-refresh finding; edges done (75a6e0e)
|
||||||
|
- finding — Resume planning session is broken (session_id never captured) + error swallowed by empty catch (9efc5c9)
|
||||||
|
- §1 DiffModal error-state resolved via code analysis (defensive/unreachable, gates prevent it) (6ca8cac)
|
||||||
|
- §4 merge-editor Abort PASS (tree clean, task stays WaitingForReview) (9a1fa3d)
|
||||||
|
- §9 attachments drag&drop UI PASS (overlay/drop/picker/remove); finding — intermittent first-drop error (9b4d343)
|
||||||
|
- §8 Session Skills complete (Remove PASS); refresh handoff summary + fixture state (a1ba3b6)
|
||||||
|
- §8 per-task activation + no-leak counterprobe PASS; UI partial (cards/general-tab open) (9b041ba)
|
||||||
|
- finding — agent-settings gear uses Unicode glyph, not Icon.Settings PathIcon (inconsistent) (39fc594)
|
||||||
|
- §8 skill install PASS (6 skills, commit-pinned); finding — Skills tab has no empty-state (b353ed6)
|
||||||
|
- note Mika explicitly wants AskUser interaction in detail island (416e47e)
|
||||||
|
- §7 AskUser complete — timeout UI-cleanup visually verified (banner clears) (e8b5e97)
|
||||||
|
- §7 AskUser PASS (happy-path + backend timeout); finding — banner only in Mission Control, absent in detail island (d19ef54)
|
||||||
|
- note fixture cleanup (verif tasks/worktrees removed, ClaudeDoTests reset) (ded068c)
|
||||||
|
- refresh handoff for next session — progress, remaining (§7-§11+edges), gotchas, fixture state (9556e0e)
|
||||||
|
- trim to actively-verified 2026-07-24 findings; drop stale manual-verif/historical blocks (9d39a8f)
|
||||||
|
- §5 ConPTY/Mission Control PASS (prompt-send, close kills proc); ad-hoc icon invisible + re-open re-sends noted (b536b6f)
|
||||||
|
- §5 findings — invisible New-session icon (Icon.Plus stroke-only), re-open re-sends prompt (1b80bb0)
|
||||||
|
- §3 PASS end-to-end (+§1 children-band, +§4 planning-conflict); dequeue-X UX nit (4394623)
|
||||||
|
- §3 finalize findings — improvements-mislabel, child-badge live-refresh, chain not visualized (f0b0582)
|
||||||
|
- planning session permission-prompt bug + planning-active parent shows Idle (UX) (07de897)
|
||||||
|
- clean additive approve PASS (2101228)
|
||||||
|
- §1 commit-range-after-merge PASS; blocked-merge silent-fail confirmed on clean path too (8241bf8)
|
||||||
|
- §4 merge editor PASS end-to-end + UX findings (continue-btn, multi-file, blocked-merge) (255705d)
|
||||||
|
- Approve & Merge silently swallows a blocked merge (no footer error) (3dfd75f)
|
||||||
|
- §1 findings — raw-JSON outcome bug, rename/turns nits, session-tab expected (26c03a5)
|
||||||
|
- correct permission finding — auto+haiku denies writes (not a CLI regression), §2 happy-path PASS (3211bfc)
|
||||||
|
- log autonomous-batch results (§2/§6/§9/§12) (79ce7af)
|
||||||
|
- track CLI 2.1.207 --permission-mode auto write-denial regression (0ad93f4)
|
||||||
|
- remove chain-cascade bug bullet (fixed in 110364a) (3d668da)
|
||||||
|
- explore-notes convention + verification handoff for manual checks (d6891b8)
|
||||||
|
- update for v2.1.0 (ad58129)
|
||||||
|
|
||||||
|
## v2.1.0 — 2026-07-23
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- seed fresh task session with the task prompt (d8194ad)
|
||||||
|
- New session button for ad-hoc ConPTY sessions (3feb08d)
|
||||||
|
- fresh-worktree-on-demand + ad-hoc launch specs (9ab48d7)
|
||||||
|
- host task-based ConPTY sessions in Command Center (0513265)
|
||||||
|
- embedded ConPTY terminal host in UI (5f740c0)
|
||||||
|
- worker launch-spec for embedded ConPTY sessions (1245e75)
|
||||||
|
- session skills registry tab + per-level selectors (7c3c061)
|
||||||
|
- session skills SignalR surface + per-level persistence (b4c5808)
|
||||||
|
- resolve and seed session skills before each run (4626481)
|
||||||
|
- session skill registry (install/update/remove, pinned clone) (dea2b7d)
|
||||||
|
- session skills entity, repository, and migration (54cdaf8)
|
||||||
|
- pick up a task's session in a terminal (eb88dc1)
|
||||||
|
- resume a task's claude session in a terminal (140ae2f)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- set monospace font + stretch on ConPTY terminal (d9a4627)
|
||||||
|
- use library LaunchProcess instead of custom pty bypass (2b06ab0)
|
||||||
|
- correct ConPTY terminal size + reduce lag (bb62740)
|
||||||
|
- forward keyboard input to ConPTY terminal (25922a2)
|
||||||
|
- seed planning brief via file to avoid newline truncation (865e12c)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- remove streaming interactive stack (superseded by ConPTY) (c412a84)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update ConPTY spec + open items to final state (85c7e65)
|
||||||
|
- record ConPTY library + binding decision from spike (d28c63d)
|
||||||
|
- ConPTY interactive sessions spec + plan (d91ad2d)
|
||||||
|
- session skills verification items (f33838d)
|
||||||
|
- mark cwd-skill discovery verified in headless mode (dbaefe9)
|
||||||
|
- revise for multi-skill plugin repos (ponytail) (62b245a)
|
||||||
|
- spec + plan for per-level session skills (4e5057d)
|
||||||
|
- pick up a task's session in a terminal — verification (1bf08ec)
|
||||||
|
- update for v2.0.0 (914fa5a)
|
||||||
|
|
||||||
|
## v2.0.0 — 2026-06-26
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- remove a queued interactive message with a ✕ (afe7218)
|
||||||
|
- remove a queued interactive message (fd1e38f)
|
||||||
|
- show queued interactive messages above the composer (7c9ff18)
|
||||||
|
- broadcast interactive message queue + delivery (84034e8)
|
||||||
|
- highlight user chat messages + opt-in interrupt (stop) button (786eb28)
|
||||||
|
- queue interactive messages by default, interrupt opt-in (bdda98e)
|
||||||
|
- interactive chat composer in the session terminal + work console (1fe72a1)
|
||||||
|
- interactive chat composer state on the session monitor VM (140b8e1)
|
||||||
|
- worker client surface for in-app interactive sessions (9effdde)
|
||||||
|
- in-app interactive session service, replacing the wt terminal launch (30e87e6)
|
||||||
|
- persistent streaming Claude session + live session registry (d8a043f)
|
||||||
|
- answer a running task's question inline in Mission Control (917301d)
|
||||||
|
- AskUser MCP tool so a running task can ask the user mid-run (c7f8280)
|
||||||
|
- replace OLE task-row drag with custom ghost drag (bec26b2)
|
||||||
|
- ghost-window drag infrastructure for task rows (05aec8e)
|
||||||
|
- drag a task into Mission Control to queue it (3b629c2)
|
||||||
|
- read-only queue side strip in Mission Control (9eb54a0)
|
||||||
|
- batch MCP tools for the external endpoint (1c94fbd)
|
||||||
|
- open Settings from the Mission Control header (7f4dc8b)
|
||||||
|
- drag-reorder Mission Control panes by their header (f6ecfc9)
|
||||||
|
- mission control pane header actions + status tinting (e2fad88)
|
||||||
|
- mission control detach/redock toggle, clear review panes, reorder helper (fbcffce)
|
||||||
|
- detach a monitor into its own window (5f6e748)
|
||||||
|
- open Mission Control from the title bar (b1bd912)
|
||||||
|
- add MissionControl window + grid (283310a)
|
||||||
|
- add MonitorPaneView (15a3e65)
|
||||||
|
- reveal a task by id from anywhere (5a21d67)
|
||||||
|
- add MissionControlViewModel (42da840)
|
||||||
|
- extract TaskMonitorViewModel streaming core; DetailsIsland delegates (aa7a49f)
|
||||||
|
- collapse parent task rows by default with granular row sync (38defee)
|
||||||
|
- shell-style review prompt line in WorkConsole (0a119f1)
|
||||||
|
- Log Visualizer overlay reachable from a clickable footer log line (c4f74a7)
|
||||||
|
- route Serilog Warn/Error to footer + buffer recent logs for overlay (08a4f97)
|
||||||
|
- segmented Description/Steps/Files header (9301bbc)
|
||||||
|
- drag-and-drop file attachments on the detail pane (d8ff8cc)
|
||||||
|
- MCP tools to attach/list/remove task files (f7e946e)
|
||||||
|
- inject reference files into the run + clean up files on delete (6a0c0f5)
|
||||||
|
- data layer for task file attachments (3f9f047)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- reap idle interactive sessions so they don't pile up (711374e)
|
||||||
|
- kill spawned claude trees when the worker dies (faf6104)
|
||||||
|
- scroll revealed task into view + stronger selection highlight (f63be28)
|
||||||
|
- persist Online Inbox tab on settings save (66907d2)
|
||||||
|
- paint accent buttons with moss tokens instead of Fluent blue (178fd25)
|
||||||
|
- make worktree state chips readable with on-theme tints (df84fc3)
|
||||||
|
- keep interactive & planning prompts intact past Windows Terminal (ea16da2)
|
||||||
|
- honor runtime disable in sync loop to stop OIDC discovery (f86b785)
|
||||||
|
- surface interactive/planning launch errors in footer (134b9fb)
|
||||||
|
- render X remove icon as filled geometry (637886f)
|
||||||
|
- hide batch Merge All in the global overview (5231ad6)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- split LogLineViewModel into its own file (7b6a8f0)
|
||||||
|
- single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff (167d2fe)
|
||||||
|
- single AgentConfigEditor for list + task scopes (eb0ddb5)
|
||||||
|
- drop self-update, publish stable-named ClaudeDo.Installer.exe (3cb4802)
|
||||||
|
- single IMergeCoordinator replaces the 5 conflict seams (5be4b5c)
|
||||||
|
- single IDialogService replaces scattered Show* dialog seams (d598a53)
|
||||||
|
- route UI quick-add through TaskRepository.AddAsync (1fb2e34)
|
||||||
|
- drop dead hunks conflict API (b3e099c)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- queued messages can be removed via ✕ (3eea2b7)
|
||||||
|
- queued messages show in a pending strip above the composer (e7fa373)
|
||||||
|
- interactive send=queue, interrupt opt-in via stop button (8e1732a)
|
||||||
|
- manual-verification items for in-app interactive sessions (9c292e5)
|
||||||
|
- spec + plan for in-app interactive sessions (10342bc)
|
||||||
|
- spec + plan for answering Claude's mid-run questions in Mission Control (946d26c)
|
||||||
|
- add Mission Control multi-task monitoring spec + plan (d80a578)
|
||||||
|
- document footer log routing + Log Visualizer overlay (4022bd7)
|
||||||
|
- spec + plan for worker-log footer routing and log visualizer overlay (60eb671)
|
||||||
|
- document task file attachments across project docs (8716dd8)
|
||||||
|
- spec + phased plan for one-component-per-feature (0993eb0)
|
||||||
|
- update for v1.9.0 (bae8921)
|
||||||
|
|
||||||
## v1.9.0 — 2026-06-19
|
## v1.9.0 — 2026-06-19
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Each project has its own `CLAUDE.md` — those are the living per-project docs.
|
|||||||
- EF Core migrations manage schema (Migrations/ folder in ClaudeDo.Data)
|
- EF Core migrations manage schema (Migrations/ folder in ClaudeDo.Data)
|
||||||
- `IDbContextFactory<ClaudeDoDbContext>` used by singleton consumers (e.g. Worker)
|
- `IDbContextFactory<ClaudeDoDbContext>` used by singleton consumers (e.g. Worker)
|
||||||
- Entity configuration via `IEntityTypeConfiguration<T>` in Configuration/ folder
|
- Entity configuration via `IEntityTypeConfiguration<T>` in Configuration/ folder
|
||||||
- Task status flow: Idle | Queued -> Running -> WaitingForReview -> Done | Failed | Cancelled. A task that spawns/has children passes through WaitingForChildren first, then surfaces for review once every child is terminal — this is the single parent model for both planning and improvement parents (planning/improvement *children* themselves go straight to Done, only the parent is reviewed). From review you can approve, reject-rerun (Queued, resumes the session with feedback), reject-park (Idle), or cancel. Approve is the single review+merge action: a childless task merges its own worktree then Done (conflicts keep it in WaitingForReview); a task with children drives the unit merge (parent worktree if any + each Done child in order, with conflict continue/abort). Tasks with no active worktree (sandbox run) approve straight to Done.
|
- Task status flow: Idle | Queued -> Running -> WaitingForReview -> Done | Failed | Cancelled. A task that spawns/has children passes through WaitingForChildren first, then surfaces for review once every child is terminal — this is the single parent model for both planning and improvement parents (planning/improvement *children* themselves go straight to Done, only the parent is reviewed). From review you can approve, reject-rerun (Queued, resumes the session with feedback), reject-park (Idle), or cancel. Approve is the single review+merge action: a childless task merges its own worktree then Done (conflicts keep it in WaitingForReview); a task with children drives the unit merge (parent worktree if any + each Done child in order, with conflict continue/abort). Tasks with no active worktree (sandbox run) approve straight to Done. In the detail pane, **Approve & Merge is gated behind opening the diff**: when there is something to inspect (worktree diff / merged range / children combined diff) the button stays disabled until the diff or combined-diff viewer has been opened once, and re-locks per run (any state change resets it); tasks with nothing to inspect are never gated. The row-level quick-approve in the task list is an intentional bypass.
|
||||||
- Worktree state flow: Active -> Merged | Discarded | Kept
|
- Worktree state flow: Active -> Merged | Discarded | Kept
|
||||||
- The queue picker claims tasks by `Status=Queued` (with `BlockedByTaskId IS NULL`); the legacy tag system was removed
|
- The queue picker claims tasks by `Status=Queued` (with `BlockedByTaskId IS NULL`); the legacy tag system was removed
|
||||||
- Interfaces live in an `Interfaces/` subfolder beside their consumers (namespace unchanged)
|
- Interfaces live in an `Interfaces/` subfolder beside their consumers (namespace unchanged)
|
||||||
@@ -84,3 +84,4 @@ dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
|||||||
- `docs/improvement-plan.md` — improvement snapshot from 2026-04-13 (historical)
|
- `docs/improvement-plan.md` — improvement snapshot from 2026-04-13 (historical)
|
||||||
- `docs/prompts-inventory.md`, `docs/mailbox-proposal.md` — reference material (mailbox integration is parked)
|
- `docs/prompts-inventory.md`, `docs/mailbox-proposal.md` — reference material (mailbox integration is parked)
|
||||||
- `CHANGELOG.md` — Keep a Changelog format, maintained on release
|
- `CHANGELOG.md` — Keep a Changelog format, maintained on release
|
||||||
|
- `docs/explore-notes/` — distilled maps of complex subsystems from deep exploration (detail too fine for a CLAUDE.md). **Before** deep-exploring a subsystem, check for a matching note first; **after** a deep explore, distill durable findings back and bump its "verified against" commit. Always verify against current code before trusting. See `docs/explore-notes/README.md`.
|
||||||
|
|||||||
@@ -1,106 +1,429 @@
|
|||||||
# ClaudeDo
|
# ClaudeDo
|
||||||
|
|
||||||
A desktop task management app that executes tasks autonomously via [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) in isolated git worktrees.
|
A Windows desktop app that turns your to-do list into a work queue for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
||||||
|
|
||||||
Queue up coding tasks, and ClaudeDo picks them up one by one — each running in its own worktree so your main branch stays clean.
|
Write down what you want done. ClaudeDo picks the task up, runs Claude in an isolated git
|
||||||
|
worktree, and hands you back a diff to review. Your main branch is never touched until you
|
||||||
|
approve.
|
||||||
|
|
||||||
## Architecture
|
It looks and feels like a normal task app — lists, My Day, stars, due dates — except every
|
||||||
|
task can also be *executed*.
|
||||||
|
|
||||||
Two-process system communicating over SignalR:
|
---
|
||||||
|
|
||||||
| Project | Role |
|
## Contents
|
||||||
|---|---|
|
|
||||||
| **ClaudeDo.App** | Avalonia desktop entry point, DI container setup |
|
|
||||||
| **ClaudeDo.Ui** | Views, ViewModels, SignalR client (MVVM) |
|
|
||||||
| **ClaudeDo.Data** | SQLite data layer, repositories, models, GitService |
|
|
||||||
| **ClaudeDo.Worker** | ASP.NET Core hosted service, task queue, Claude CLI runner |
|
|
||||||
|
|
||||||
```
|
- [Is this for you?](#is-this-for-you)
|
||||||
┌────────────────┐ SignalR ┌────────────────┐
|
- [Requirements](#requirements)
|
||||||
│ ClaudeDo.App │◄───────────►│ ClaudeDo.Worker │
|
- [Install](#install)
|
||||||
│ (Avalonia) │ 127.0.0.1 │ (ASP.NET Core) │
|
- [The window](#the-window)
|
||||||
│ │ :47821 │ │
|
- [The core loop](#the-core-loop)
|
||||||
│ ┌────────────┐│ │ ┌────────────┐ │
|
- [Lists and repositories](#lists-and-repositories)
|
||||||
│ │ Ui ││ │ │ TaskQueue │ │
|
- [Writing a task](#writing-a-task)
|
||||||
│ │(ViewModels)││ │ │ Claude CLI │ │
|
- [Ways to run a task](#ways-to-run-a-task)
|
||||||
│ └────────────┘│ │ └────────────┘ │
|
- [Watching work happen](#watching-work-happen)
|
||||||
└───────┬────────┘ └───────┬────────┘
|
- [Reviewing and merging](#reviewing-and-merging)
|
||||||
│ │
|
- [Resolving conflicts](#resolving-conflicts)
|
||||||
└──────────────┬───────────────┘
|
- [Worktrees](#worktrees)
|
||||||
│
|
- [Staying inside your usage limits](#staying-inside-your-usage-limits)
|
||||||
┌───────┴───────┐
|
- [Your daily rhythm](#your-daily-rhythm)
|
||||||
│ ClaudeDo.Data │
|
- [Settings](#settings)
|
||||||
│ (SQLite) │
|
- [Letting Claude drive ClaudeDo](#letting-claude-drive-claudedo)
|
||||||
└───────────────┘
|
- [Updates](#updates)
|
||||||
```
|
- [Where your data lives](#where-your-data-lives)
|
||||||
|
- [Keyboard shortcuts](#keyboard-shortcuts)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [For developers](#for-developers)
|
||||||
|
|
||||||
## Tech Stack
|
---
|
||||||
|
|
||||||
- .NET 8.0
|
## Is this for you?
|
||||||
- Avalonia 12.0.0 (Fluent theme)
|
|
||||||
- SQLite (WAL mode) via Entity Framework Core (EF Core + Migrations)
|
|
||||||
- SignalR for real-time IPC between UI and Worker
|
|
||||||
- CommunityToolkit.Mvvm for source-generated MVVM
|
|
||||||
- Git worktrees for task isolation
|
|
||||||
|
|
||||||
## Prerequisites
|
ClaudeDo is built for one person running many small-to-medium coding jobs across several
|
||||||
|
repositories, mostly unattended.
|
||||||
|
|
||||||
- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
It fits when you:
|
||||||
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
|
||||||
|
- have a backlog of contained changes ("rename this", "add that endpoint", "fix this bug")
|
||||||
|
- want them worked on while you do something else
|
||||||
|
- still want to read every diff before it lands on `main`
|
||||||
|
|
||||||
|
It is *not* a CI system, not a team tool, and not a chat window. There are no pull
|
||||||
|
requests, no reviewers but you, and no cloud component (unless you deliberately turn on the
|
||||||
|
optional online inbox).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows 10/11
|
||||||
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), installed and signed in
|
||||||
|
(ClaudeDo runs `claude` as *you* — it never handles your credentials)
|
||||||
- Git
|
- Git
|
||||||
|
- [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) — the installer
|
||||||
|
itself needs it, so install it first if the installer refuses to start
|
||||||
|
|
||||||
## Getting Started
|
## Install
|
||||||
|
|
||||||
|
Run `ClaudeDo.Installer.exe`. It downloads the current release, writes its config, and
|
||||||
|
starts the background worker.
|
||||||
|
|
||||||
|
The wizard asks for:
|
||||||
|
|
||||||
|
| Page | What it decides |
|
||||||
|
|---|---|
|
||||||
|
| **Welcome** | Install folder, and whether Claude may manage your tasks via MCP (see [below](#letting-claude-drive-claudedo)) |
|
||||||
|
| **Data paths** | Where the database, logs, sandboxes and worktrees live |
|
||||||
|
| **Worker** | Port, path to the `claude` binary, and whether the worker starts at logon |
|
||||||
|
|
||||||
|
Re-running the installer later gives you **Update**, **Repair** and **Uninstall** instead
|
||||||
|
of the full wizard. Uninstall asks separately before deleting your tasks and settings.
|
||||||
|
|
||||||
|
ClaudeDo has two parts: the window you see, and a background **worker** that does the
|
||||||
|
actual running. The worker starts at logon and keeps going even when the window is closed —
|
||||||
|
so a long task finishes whether you are watching or not.
|
||||||
|
|
||||||
|
## The window
|
||||||
|
|
||||||
|
Three panes ("islands"), plus a footer:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┬────────────────────────────┬───────────────────────────┐
|
||||||
|
│ LISTS │ TASKS │ DETAILS │
|
||||||
|
│ │ │ │
|
||||||
|
│ My Day │ ▸ Add a task… ENTER │ Fix login redirect │
|
||||||
|
│ Important │ │ ───────────────────── │
|
||||||
|
│ Planned │ OVERDUE │ Steps ▢ ▢ ▣ │
|
||||||
|
│ │ ● Fix login redirect │ Details (markdown) │
|
||||||
|
│ Queue 3 │ │ Files (drop here) │
|
||||||
|
│ Running 1 │ TASKS │ │
|
||||||
|
│ Review 2 │ ● Add CSV export RUNNING │ Worktree · Diff · Merge │
|
||||||
|
│ │ ● Bump deps QUEUED │ │
|
||||||
|
│ MY LISTS │ ● Write changelog │ Output │ Git │ Session │
|
||||||
|
│ LagerApp 7 │ ● Call the tax guy MANUAL │ ┌─────────────────────┐ │
|
||||||
|
│ LogX 2 │ │ │ live Claude output │ │
|
||||||
|
│ ClaudeDo 12 │ │ └─────────────────────┘ │
|
||||||
|
├──────────────┴────────────────────────────┴───────────────────────────┤
|
||||||
|
│ ● Online 5h 41% · 7d 22% worker log line… logs │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Lists** (left) — smart lists, virtual work lists, and your own lists (one per repo)
|
||||||
|
- **Tasks** (middle) — the selected list, grouped into Overdue / Tasks / Completed
|
||||||
|
- **Details** (right) — everything about the selected task, including its live output
|
||||||
|
- **Footer** — worker connection, usage pill, and the latest worker log line (clickable)
|
||||||
|
|
||||||
|
On a narrow window the side panes fold away automatically.
|
||||||
|
|
||||||
|
The grid icon in the title bar opens **Mission Control**, a separate window for watching
|
||||||
|
several running sessions at once.
|
||||||
|
|
||||||
|
## The core loop
|
||||||
|
|
||||||
|
```
|
||||||
|
write it down → queue it → Claude runs it → you review → merge
|
||||||
|
(Idle) (Queued) (Running) (Waiting for (Done)
|
||||||
|
Review)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Capture.** Type into the add box. That is it — a task starts out as a plain reminder.
|
||||||
|
2. **Queue.** Right-click → *Send to queue*. The worker claims queued tasks in order, as
|
||||||
|
many at a time as *Max parallel executions* allows.
|
||||||
|
3. **Run.** ClaudeDo creates a git worktree off your repo (branch `claudedo/<id>`), runs
|
||||||
|
Claude there with your task as the prompt, and streams the output into the detail pane.
|
||||||
|
4. **Review.** On success the task lands in **Waiting for Review** with a committed
|
||||||
|
worktree and a diff. Nothing has touched your working copy.
|
||||||
|
5. **Merge.** *Approve & merge* merges the branch into the target you pick, then marks the
|
||||||
|
task Done. Or reject it with feedback, park it, or cancel it.
|
||||||
|
|
||||||
|
A task that fails is marked **Failed** and keeps its worktree and its log, so you can look
|
||||||
|
at what happened and either continue the same session or reset and retry from scratch.
|
||||||
|
|
||||||
|
## Lists and repositories
|
||||||
|
|
||||||
|
A list becomes *runnable* by giving it a **working directory** — a git repo. Tasks in that
|
||||||
|
list get worktrees off that repo; tasks in a list without a working directory can still be
|
||||||
|
run, but in a scratch sandbox with no git.
|
||||||
|
|
||||||
|
- **Add repos as lists** (`Repositories` menu, or the folder icon) scans folders you point
|
||||||
|
it at and creates one list per git repo it finds. This is the fastest way to get started.
|
||||||
|
- **List settings** (right-click a list) sets the name, working directory, default commit
|
||||||
|
type, the agent defaults for that list, and an optional verify command.
|
||||||
|
- **Manual list** — mark a list as reminders-only. New tasks in it start out manual, so
|
||||||
|
automation never touches them. Good for a "Errands" or "Phone calls" list.
|
||||||
|
- Drag a task onto another list to move it. ClaudeDo refuses to move a running task and
|
||||||
|
warns you when the two lists point at different repos.
|
||||||
|
|
||||||
|
**Smart lists** are always there:
|
||||||
|
|
||||||
|
| List | Contains |
|
||||||
|
|---|---|
|
||||||
|
| **My Day** | What you (or the daily prep) picked for today, plus a pinned Notes row |
|
||||||
|
| **Important** | Starred tasks |
|
||||||
|
| **Planned** | Everything with a date |
|
||||||
|
| **Queue / Running / Review** | Live work — waiting, in flight, and awaiting your review |
|
||||||
|
|
||||||
|
## Writing a task
|
||||||
|
|
||||||
|
The detail pane is where a one-line reminder becomes a brief Claude can act on:
|
||||||
|
|
||||||
|
- **Title + Details** — markdown, with an edit/preview toggle. This is the prompt.
|
||||||
|
- **Steps** — a checklist. Handy for you, and included when you copy the task out.
|
||||||
|
- **Files** — drag files onto the pane (or use *Add file…*) to attach reference material.
|
||||||
|
Attachments are handed to Claude as read-only paths, not pasted into the prompt.
|
||||||
|
- **Star / Schedule / Add to My Day** — the usual task-app affordances.
|
||||||
|
- **Agent settings** — per-task overrides for model, turn budget, extra system prompt,
|
||||||
|
agent file and skills. Anything you do not override shows an *inherited* badge pointing at
|
||||||
|
the list or global default.
|
||||||
|
- **Refine** — hand the task to Claude to sharpen it: it rewrites the title and details into
|
||||||
|
a proper brief. Useful when you captured something in three words.
|
||||||
|
- **Mark as manual** — a MANUAL badge; the queue, the daily prep and every automation skip
|
||||||
|
it. Use it for things only you can do.
|
||||||
|
|
||||||
|
## Ways to run a task
|
||||||
|
|
||||||
|
| Way | What it does | Use it when |
|
||||||
|
|---|---|---|
|
||||||
|
| **Send to queue** | Worker picks it up in order | The normal path |
|
||||||
|
| **Run now** | Skips the queue, starts immediately | You want this one first |
|
||||||
|
| **Continue** | Resumes the last session with a follow-up | "Almost right, now also…" |
|
||||||
|
| **Reset & retry** | Throws the worktree away, re-queues from scratch | The run went sideways |
|
||||||
|
| **Open ConPTY session** | Opens the *real* Claude terminal for this task in Mission Control | You want to drive it yourself, with the task as the starting brief |
|
||||||
|
| **Open planning session** | An interactive session whose job is to break the task into subtasks | The work is too big for one run |
|
||||||
|
| **Let Claude handle it** | Hands a whole list over in one go | You have a pile of small tasks |
|
||||||
|
|
||||||
|
### Planning sessions
|
||||||
|
|
||||||
|
A planning session is a conversation whose output is *structure*, not code. Claude creates
|
||||||
|
child tasks under the parent. You then:
|
||||||
|
|
||||||
|
1. **Finalize plan** — children are chained (each waits for the previous one) but stay Idle.
|
||||||
|
2. **Queue plan** — when you are happy with the list, this queues them all.
|
||||||
|
3. The parent waits in **Waiting for Subtasks** until every child is terminal, then surfaces
|
||||||
|
for review once — you review and merge the *whole unit*, not each child.
|
||||||
|
|
||||||
|
A parked planning session is remembered; the next launch offers to resume, finalize or
|
||||||
|
discard it.
|
||||||
|
|
||||||
|
### "Let Claude handle it"
|
||||||
|
|
||||||
|
Right-click a list → *Let Claude handle it*. You get a checkbox list of that list's open
|
||||||
|
tasks. Confirm, and one Claude session works the selection end to end: it reads them,
|
||||||
|
de-duplicates overlaps, enhances thin descriptions, queues the work, and merges the results.
|
||||||
|
It shows up as its own MANUAL task so you can see what a given run covered, and you review
|
||||||
|
the combined diff when it is finished.
|
||||||
|
|
||||||
|
## Watching work happen
|
||||||
|
|
||||||
|
- **Detail pane, three tabs** — *Output* (live Claude stream), *Git* (worktree, diff,
|
||||||
|
merge), *Session* (result, token/turn counts, subtask outcomes).
|
||||||
|
- **Mission Control** — one tile per running or interactive session, in Focus or Overview
|
||||||
|
mode. ConPTY tiles are the real Claude TUI, embedded: keyboard, colors, `/` commands, all
|
||||||
|
of it. *New session* opens an ad-hoc one that belongs to no task.
|
||||||
|
- **Roadblocks** — when a run gets stuck it reports a roadblock instead of silently failing.
|
||||||
|
The Session tab shows it as its own card with a reply box: answer the question and the
|
||||||
|
same session picks up where it stopped.
|
||||||
|
- **Claude is asking** — an interactive session can put a question in front of you mid-run;
|
||||||
|
answer it inline in Mission Control and the run continues.
|
||||||
|
- **Footer log strip** — the worker's latest event. Failures of your own actions flash here
|
||||||
|
too, instead of vanishing. Click it for the last 30 minutes of worker logs, with a
|
||||||
|
warnings-and-errors filter.
|
||||||
|
|
||||||
|
## Reviewing and merging
|
||||||
|
|
||||||
|
When a task reaches **Waiting for Review** you get four actions:
|
||||||
|
|
||||||
|
| Action | Result |
|
||||||
|
|---|---|
|
||||||
|
| **Approve & merge** | Merges the work into the target branch, then Done |
|
||||||
|
| **Reject** | Asks for feedback and re-runs the same session with it |
|
||||||
|
| **Park** | Back to Idle so you can rewrite the task yourself |
|
||||||
|
| **Cancel** | Done with it; the worktree stays until you clean it up |
|
||||||
|
|
||||||
|
**You have to look before you approve.** When there is a diff to inspect, *Approve & merge*
|
||||||
|
stays disabled until you have opened the diff viewer once. It re-locks after any new run.
|
||||||
|
(The quick-approve on the task row itself is a deliberate bypass for when you already know.)
|
||||||
|
|
||||||
|
The diff viewer shows a file tree on the left and the diff on the right, and can show a
|
||||||
|
dirty worktree, a branch against its base, or a commit range. For a parent with children,
|
||||||
|
*Review combined diff* shows per-subtask diffs plus a combined preview of the whole unit.
|
||||||
|
|
||||||
|
**Verify command** (optional, per list) — a command that must exit 0 after a merge lands
|
||||||
|
before the task is allowed to reach Done. Typically a build or a test run. If it fails, the
|
||||||
|
merge stays (nothing is rewritten behind your back) but the task is held out of Done and the
|
||||||
|
failure output is reported.
|
||||||
|
|
||||||
|
## Resolving conflicts
|
||||||
|
|
||||||
|
If a merge conflicts, ClaudeDo opens a three-pane merge editor in-app:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┬──────────────────┬──────────────────┐
|
||||||
|
│ MAIN │ RESULT │ INCOMING │
|
||||||
|
│ merge target │ (editable) │ task branch │
|
||||||
|
│ │ ▐ │ │
|
||||||
|
│ › │ ← you build │ ‹ │
|
||||||
|
│ │ the result │ │
|
||||||
|
└──────────────────┴──────────────────┴──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Whole files, syntax-highlighted, scrolling in sync
|
||||||
|
- Each conflict starts *empty*. The gutter arrows toggle each side in or out — take main,
|
||||||
|
incoming, both (in the order you click), or neither
|
||||||
|
- Only conflict regions are editable in the middle pane; type your own resolution if
|
||||||
|
neither side is right
|
||||||
|
- `F8` / `Shift+F8` jump between conflicts; the ruler beside the result pane maps every
|
||||||
|
conflict in the file
|
||||||
|
- A counter tells you how many files still need attention. *Resolve & continue* is disabled
|
||||||
|
until they are all done, and *Abort merge* always leaves the target branch as it was
|
||||||
|
|
||||||
|
Binary conflicts cannot be resolved here — ClaudeDo says so and lists the paths.
|
||||||
|
|
||||||
|
## Worktrees
|
||||||
|
|
||||||
|
Every run gets its own worktree, so parallel tasks never fight over your checkout.
|
||||||
|
|
||||||
|
The **Worktrees** overview (per list or global) lists them with state, diff size, age and
|
||||||
|
outcome. From there you can show a diff, jump to the task, merge, batch-merge a selection
|
||||||
|
into one target, mark one *Kept* or *Discarded*, or force-remove a leftover. Worktrees whose
|
||||||
|
directory is gone from disk are flagged as *phantom*.
|
||||||
|
|
||||||
|
Finished worktrees are auto-cleaned after a number of days you choose in Settings.
|
||||||
|
|
||||||
|
## Staying inside your usage limits
|
||||||
|
|
||||||
|
The footer pill shows your Claude usage: `5h 41% · 7d 22%`. Click it for the **Usage
|
||||||
|
Monitor**:
|
||||||
|
|
||||||
|
- Gauges for the 5-hour session window and the 7-day windows, each with a reset countdown
|
||||||
|
- **Models** — token usage per model over a range you pick, split into ClaudeDo's own
|
||||||
|
consumption and everything else
|
||||||
|
- **Tasks** — your biggest consumers, with run counts and totals
|
||||||
|
|
||||||
|
And the part that matters when you are asleep: the **usage limit stop**. Set a percentage
|
||||||
|
per window in Settings, and once usage reaches it the queue simply stops picking up new
|
||||||
|
tasks. Running tasks finish normally, and the queue resumes by itself as the window rolls
|
||||||
|
over. `0` turns a stop off. Manual actions — *Run now*, *Continue*, interactive and planning
|
||||||
|
sessions — are never blocked.
|
||||||
|
|
||||||
|
## Your daily rhythm
|
||||||
|
|
||||||
|
- **My Day** — the day's shortlist. *Clear day* empties it.
|
||||||
|
- **Prime Claude** (Settings → Prime Claude) — schedules for the days and times you pick.
|
||||||
|
At that time ClaudeDo runs a short prep session: Claude looks at your idle tasks, picks an
|
||||||
|
effort-aware subset up to your daily cap, and fills My Day. It also warms your usage
|
||||||
|
window for the day. Only runs while ClaudeDo is open; if you start the app within 30
|
||||||
|
minutes of a scheduled time it fires right away. *Plan day* runs it on demand.
|
||||||
|
- **Notes** — a pinned row in My Day: dated bullet notes with a day navigator, for the
|
||||||
|
thoughts that are not tasks.
|
||||||
|
- **Weekly report** (`Help` menu) — pick a range (defaults to "since your standup weekday")
|
||||||
|
and Claude writes up what actually happened, from your task and session history. Paths you
|
||||||
|
do not want in reports can be excluded in Settings.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Most settings exist at three levels — **global → list → task** — and the more specific one
|
||||||
|
wins. Overridden fields carry an *override* badge with a one-click reset; inherited ones say
|
||||||
|
where they come from.
|
||||||
|
|
||||||
|
**General**
|
||||||
|
- Default instructions applied to every task
|
||||||
|
- Default model, and a **per-model table** of reasoning effort and turn budget — so `haiku`
|
||||||
|
can be cheap and short while `opus` gets room to think
|
||||||
|
- Permission mode for autonomous runs
|
||||||
|
- Max parallel executions
|
||||||
|
- Usage limit stops (see above)
|
||||||
|
- Session skills applied to every task
|
||||||
|
- Report exclusions, standup weekday
|
||||||
|
- Accent color (Moss / Peat / Sea) and language (English / German)
|
||||||
|
|
||||||
|
**Worktrees** — sibling or central placement, auto-cleanup age, and the manual cleanup and
|
||||||
|
force-remove-all buttons.
|
||||||
|
|
||||||
|
**Files** — open the prompt templates (system, planning, retry, daily prep, weekly report)
|
||||||
|
in your editor, and restore the bundled default agent files.
|
||||||
|
|
||||||
|
**Skills** — install a skill from a git URL, update it, remove it. Skills selected globally,
|
||||||
|
per list and per task combine.
|
||||||
|
|
||||||
|
**Prime Claude** — the schedules and the daily task cap.
|
||||||
|
|
||||||
|
**Online Inbox** (optional, off by default) — mirrors your idle backlog to a server so you
|
||||||
|
can capture tasks from your phone. Disabled means zero network traffic. Enabling it needs a
|
||||||
|
URL plus a sign-in; the refresh token is stored encrypted on your machine, never in a config
|
||||||
|
file.
|
||||||
|
|
||||||
|
## Letting Claude drive ClaudeDo
|
||||||
|
|
||||||
|
If you allowed it during install, ClaudeDo registers itself as an MCP server with the Claude
|
||||||
|
CLI. Any Claude session on your machine can then read and manage your tasks: list and create
|
||||||
|
tasks, add subtasks, queue and cancel runs, read logs and diffs, merge, review, manage lists
|
||||||
|
and per-list config.
|
||||||
|
|
||||||
|
Concretely, you can sit in a normal Claude Code session and say "put these five things in
|
||||||
|
the ClaudeDo backlog for the LagerApp list", or "check whether the CSV export task is done".
|
||||||
|
|
||||||
|
The MCP surface is deliberately narrow on the dangerous end: it can start and observe work,
|
||||||
|
but it cannot rewrite your app settings, and a task with an active worktree cannot be forced
|
||||||
|
to Done behind your back.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
ClaudeDo checks for new releases and shows a banner when one is out. *Update now* relaunches
|
||||||
|
the installer, which stops the worker, swaps the binaries and starts it again. Your database,
|
||||||
|
worktrees and settings are preserved. You can also check manually via `Help → Check for
|
||||||
|
updates`.
|
||||||
|
|
||||||
|
## Where your data lives
|
||||||
|
|
||||||
|
Everything is under `%USERPROFILE%\.todo-app`:
|
||||||
|
|
||||||
|
| Path | What |
|
||||||
|
|---|---|
|
||||||
|
| `todo.db` | Your tasks, lists, runs and worktree records (SQLite) |
|
||||||
|
| `ui.config.json` | Window and UI settings |
|
||||||
|
| `worker.config.json` | Worker settings — paths, port, worktree strategy |
|
||||||
|
| `logs/` | Worker and task logs |
|
||||||
|
| `agents/` | Your agent definition files |
|
||||||
|
| `attachments/` | Files you attached to tasks |
|
||||||
|
|
||||||
|
`Help → About` links straight to these folders. Nothing leaves your machine unless you turn
|
||||||
|
on the online inbox.
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `Ctrl+K` | Focus search |
|
||||||
|
| `Ctrl+N` | Focus the add-task box |
|
||||||
|
| `Enter` | Add the task |
|
||||||
|
| `Esc` | Leave the current text field / close a dialog |
|
||||||
|
| `F8` / `Shift+F8` | Next / previous conflict (merge editor) |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Worker not reachable"** — the background worker is not running. The dialog offers to
|
||||||
|
start it; `Worker → Restart worker` does the same later. If it keeps happening, re-run the
|
||||||
|
installer and choose *Repair*.
|
||||||
|
|
||||||
|
**A task failed immediately** — usually the `claude` CLI is not signed in, or the list's
|
||||||
|
working directory is not a git repo. Check the Output tab and the footer log.
|
||||||
|
|
||||||
|
**A task is stuck in Running after a crash** — the worker sweeps orphaned runs to Failed on
|
||||||
|
startup. Restart the worker.
|
||||||
|
|
||||||
|
**Nothing is being picked up** — check the usage pill: if a usage stop is active the queue is
|
||||||
|
paused on purpose. Also check whether the tasks are MANUAL, blocked by a predecessor, or
|
||||||
|
scheduled for later.
|
||||||
|
|
||||||
|
## For developers
|
||||||
|
|
||||||
|
Architecture, build commands and per-project docs live in `CLAUDE.md` (root and one per
|
||||||
|
project under `src/`). Short version: .NET 8, Avalonia UI, SQLite via EF Core, and a
|
||||||
|
SignalR-connected worker process on `127.0.0.1:47821`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
dotnet build src/ClaudeDo.App
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
dotnet build src/ClaudeDo.Worker
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
|
||||||
# Run tests
|
|
||||||
dotnet test tests/ClaudeDo.Worker.Tests
|
|
||||||
|
|
||||||
# Run the app
|
|
||||||
dotnet run --project src/ClaudeDo.App
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. Create a task in the UI and tag it with **"agent"** to mark it for automated execution.
|
|
||||||
2. The Worker picks up queued tasks and runs each one via Claude CLI in an isolated git worktree.
|
|
||||||
3. When done, the worktree can be merged, kept for review, or discarded.
|
|
||||||
|
|
||||||
**Task status flow:** `Manual | Queued → Running → Done | Failed`
|
|
||||||
|
|
||||||
**Worktree state flow:** `Active → Merged | Discarded | Kept`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All data and config lives under `~/.todo-app/`:
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `todo.db` | SQLite database |
|
|
||||||
| `ui.config.json` | UI settings |
|
|
||||||
| `worker.config.json` | Worker settings (worktree strategy, etc.) |
|
|
||||||
| `logs/` | Application logs |
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ClaudeDo.slnx
|
|
||||||
├── src/
|
|
||||||
│ ├── ClaudeDo.App/ # Desktop entry point
|
|
||||||
│ ├── ClaudeDo.Ui/ # Views & ViewModels
|
|
||||||
│ ├── ClaudeDo.Data/ # Data access layer
|
|
||||||
│ └── ClaudeDo.Worker/ # Background task runner
|
|
||||||
├── tests/
|
|
||||||
│ └── ClaudeDo.Worker.Tests/
|
|
||||||
├── schema/
|
|
||||||
│ └── schema.sql # Database schema
|
|
||||||
└── docs/
|
|
||||||
├── plan.md # Architecture & design spec
|
|
||||||
├── open.md # Verification checklist & backlog
|
|
||||||
└── improvement-plan.md # Prioritized improvements
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Explore-notes
|
||||||
|
|
||||||
|
Distilled, reusable maps of complex subsystems, produced by deep code exploration.
|
||||||
|
The goal: stop re-exploring the same subsystem from scratch in every new session.
|
||||||
|
|
||||||
|
These sit **between** the CLAUDE.md files and the code:
|
||||||
|
|
||||||
|
- **CLAUDE.md** — high-level orientation, hand-maintained, always-loaded.
|
||||||
|
- **explore-notes** — deeper subsystem detail (flows, who-calls-whom, invariants) that is
|
||||||
|
too fine-grained for a CLAUDE.md but stable enough to be worth caching. Read on demand.
|
||||||
|
- **code** — the only source of truth.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Only stable structure.** Flows, responsibilities, entry points, invariants, relative
|
||||||
|
file paths. **No line numbers**, no exhaustive symbol dumps — those rot fastest.
|
||||||
|
- **Verify before trusting.** A note is a starting map, not authority. Always confirm
|
||||||
|
against current code before acting on it. Each note records the commit it was verified
|
||||||
|
against so you can diff for drift.
|
||||||
|
- **Not a substitute for CLAUDE.md.** If a fact belongs in orientation, put it there.
|
||||||
|
|
||||||
|
## Header every note must carry
|
||||||
|
|
||||||
|
```
|
||||||
|
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||||
|
> Last verified against commit `<short-hash>` (<date>).
|
||||||
|
> Drift check: `git log --oneline <short-hash>..HEAD -- <paths this note covers>`
|
||||||
|
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Before** deep-exploring a subsystem, check for a matching note here and read it first;
|
||||||
|
explore only to fill gaps or confirm.
|
||||||
|
2. **After** a deep explore, distill the durable findings into a new/updated note and bump
|
||||||
|
its "verified against" commit line.
|
||||||
|
3. If the drift check shows the covered paths changed a lot since the verified commit, treat
|
||||||
|
the note as suspect and re-verify the parts you rely on.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||||
|
> Last verified against commit `896d4b5` (2026-07-23).
|
||||||
|
> Drift check: `git log --oneline 896d4b5..HEAD -- src/ClaudeDo.Worker`
|
||||||
|
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||||
|
|
||||||
|
# Worker: Task Execution Pipeline
|
||||||
|
|
||||||
|
How a task moves Queued → Running → terminal, across `src/ClaudeDo.Worker`
|
||||||
|
(Queue, Runner, Lifecycle, State, Agents, Worktrees, Hub).
|
||||||
|
|
||||||
|
## End-to-End Flow (Queued → Terminal)
|
||||||
|
|
||||||
|
1. **Enqueue** — `ITaskStateService.EnqueueAsync()` (State/TaskStateService.cs)
|
||||||
|
- Idle → Queued, then wakes the dispatcher via `IQueueWaker.Wake()`.
|
||||||
|
|
||||||
|
2. **Dispatch** — `QueueService` loop (Queue/QueueService.cs)
|
||||||
|
- `BackgroundService`; waits for a wake signal or a backstop timer.
|
||||||
|
- Reads the max-parallel limit from settings; claims a free slot if under limit.
|
||||||
|
|
||||||
|
3. **Atomic Claim** — `IQueuePicker.ClaimNextAsync()` (Queue/QueuePicker.cs)
|
||||||
|
- Raw SQL `UPDATE ... RETURNING` in one transaction: picks an eligible Queued task
|
||||||
|
(unblocked, due or unscheduled; sorted by sort_order/created_at), sets status→Running
|
||||||
|
+ started_at, returns the row. Prevents two workers claiming the same task (TOCTOU).
|
||||||
|
|
||||||
|
4. **Slot Execution** — `QueueService.RunInSlotAsync()` (Queue/QueueService.cs)
|
||||||
|
- For review feedback: resume the prior session if one exists, else fold feedback into
|
||||||
|
the prompt. Calls `TaskRunner.RunAsync()` / `ContinueAsync()` with `alreadyClaimed=true`.
|
||||||
|
|
||||||
|
5. **Run Preparation** — `TaskRunner.RunAsync()` (Runner/TaskRunner.cs)
|
||||||
|
- Loads task, list config, subtasks, attachments from the DB.
|
||||||
|
- `PrepareRunDirectoryAsync()`: worktree (via WorktreeManager) if the list has a WorkingDir,
|
||||||
|
else sandbox. Generates a per-run MCP token, writes MCP config to disk.
|
||||||
|
- `StartRunningAsync()` (only if not pre-claimed): atomic Queued → Running. Broadcasts TaskStarted.
|
||||||
|
|
||||||
|
6. **Claude Execution** — `TaskRunner.RunOnceAsync()` (Runner/TaskRunner.cs)
|
||||||
|
- Creates a TaskRunEntity, points the task at the run's log path.
|
||||||
|
- Builds claude CLI args (ClaudeArgsBuilder), spawns the process via `IClaudeProcess.RunAsync()`
|
||||||
|
with prompt + working dir + streaming callback.
|
||||||
|
- Stream lines → NDJSON log + broadcast via TaskMessage. MCP tools (AskUser, SuggestImprovement)
|
||||||
|
are scoped by the per-run token.
|
||||||
|
|
||||||
|
7. **Result Handling** — `TaskRunner.HandleSuccess()` / `MarkFailed()` (Runner/TaskRunner.cs)
|
||||||
|
- Success (exit 0 + result markdown): if worktree, commit + broadcast WorktreeUpdated; then
|
||||||
|
transition to Done / WaitingForReview / WaitingForChildren (CompleteAsync / SubmitForReviewAsync
|
||||||
|
/ SubmitForChildrenAsync).
|
||||||
|
- Failure: if a session exists, auto-retry once via ContinueAsync; else MarkFailed → FailAsync.
|
||||||
|
- All terminal writes use `CancellationToken.None` so a task is never left Running.
|
||||||
|
|
||||||
|
8. **Terminal States** — `ITaskStateService` transitions (State/TaskStateService.cs)
|
||||||
|
- **Done** CompleteAsync (Running → Done) — top-level success.
|
||||||
|
- **WaitingForReview** SubmitForReviewAsync (Running → WaitingForReview) — review gate.
|
||||||
|
- **WaitingForChildren** SubmitForChildrenAsync (Running → WaitingForChildren) — blocks on children.
|
||||||
|
- **Failed** FailAsync (Running/Queued → Failed).
|
||||||
|
- **Cancelled** CancelAsync (Running/Queued/WaitingForReview/WaitingForChildren → Cancelled).
|
||||||
|
|
||||||
|
## Component Responsibilities
|
||||||
|
|
||||||
|
**Queue/**
|
||||||
|
- `QueueService` — main dispatch loop; slot limit; decides when to start tasks.
|
||||||
|
- `QueuePicker` — atomic Queued→Running claim via raw SQL.
|
||||||
|
- `QueueWaker` — semaphore for non-blocking, idempotent wake signals.
|
||||||
|
- `OverrideSlotService` — owns the RunNow / ContinueTask slot (bypasses the queue).
|
||||||
|
|
||||||
|
**Runner/**
|
||||||
|
- `TaskRunner` — orchestrates the run (prepare, execute, handle result).
|
||||||
|
- `WorktreeManager` — creates/manages git worktrees; self-heals stale branches.
|
||||||
|
- `ClaudeProcess` — spawns the claude CLI subprocess; manages streams/logs.
|
||||||
|
- `TaskRunMcpService` — runtime MCP tools (AskUser, SuggestImprovement).
|
||||||
|
- `TaskRunTokenRegistry` — per-run MCP identity for tool-access control.
|
||||||
|
- `InteractiveLaunchSpecService` — config for the task's claude run.
|
||||||
|
|
||||||
|
**State/**
|
||||||
|
- `TaskStateService` — all task status transitions; guards preconditions; signals queue/hub.
|
||||||
|
|
||||||
|
**Lifecycle/** (startup recovery)
|
||||||
|
- `StaleTaskRecovery` — tasks stuck Running after a crash/restart → Failed.
|
||||||
|
- `OrphanRecovery` — dequeues children whose parent is no longer planning (stays attached).
|
||||||
|
- `AttachmentOrphanRecovery` — cleans orphaned attachment files.
|
||||||
|
- `TaskResetService` — manual reset to Idle.
|
||||||
|
- `TaskMergeService` — conflict resolution for worktree merges.
|
||||||
|
|
||||||
|
**Hub/**
|
||||||
|
- `HubBroadcaster` — single SignalR broadcast point (TaskStarted/TaskUpdated/TaskMessage/RunCreated…).
|
||||||
|
- `WorkerHub` — SignalR hub + client methods.
|
||||||
|
|
||||||
|
**Agents/**
|
||||||
|
- `AgentFileService` — file I/O for custom agents.
|
||||||
|
- `DefaultAgentSeeder` — seeds built-in agents on startup.
|
||||||
|
|
||||||
|
**Worktrees/**
|
||||||
|
- `WorktreeMaintenanceService` — cleanup, state tracking, overview reporting.
|
||||||
|
|
||||||
|
## Entry Points & Call Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
Program.cs (DI setup)
|
||||||
|
├─ QueueService (BackgroundService) → ExecuteAsync loop
|
||||||
|
│ ├─ waits: IQueueWaker.WaitAsync() or timer
|
||||||
|
│ ├─ claims: IQueuePicker.ClaimNextAsync()
|
||||||
|
│ └─ runs: TaskRunner.RunAsync() / ContinueAsync()
|
||||||
|
├─ Hub clients → WorkerHub methods
|
||||||
|
│ ├─ Enqueue → ITaskStateService.EnqueueAsync() → Wake()
|
||||||
|
│ ├─ RunNow → OverrideSlotService.RunNow() → TaskRunner.RunAsync()
|
||||||
|
│ ├─ ContinueTask→ OverrideSlotService.ContinueTask()→ TaskRunner.ContinueAsync()
|
||||||
|
│ └─ CancelTask → QueueService.CancelTask()
|
||||||
|
├─ Lifecycle recovery (startup): StaleTaskRecovery / OrphanRecovery / AttachmentOrphanRecovery
|
||||||
|
└─ State transitions → HubBroadcaster.TaskUpdated()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invariants & Conventions
|
||||||
|
|
||||||
|
- **Atomic claiming** — QueuePicker's `UPDATE ... RETURNING` makes Queued→Running atomic.
|
||||||
|
- **Slot limit** — respects MaxParallelExecutions; a backstop timer wakes even if a Wake() is missed.
|
||||||
|
- **Pre-claimed tasks** — the dispatcher pre-claims via the picker; the override slot
|
||||||
|
(RunNow/ContinueTask) must call StartRunningAsync if a task is not pre-claimed.
|
||||||
|
- **Terminal writes** — use `CancellationToken.None`; a task is never left Running after crash/cancel.
|
||||||
|
- **Per-run MCP tokens** — each run gets a unique token scoping tool access; unregistered on end.
|
||||||
|
- **Auto-retry** — one automatic retry if a session exists and the first run failed.
|
||||||
|
- **Worktree self-heal** — on branch collision, remove phantom worktrees, prune, delete branch, retry add.
|
||||||
|
- **Review feedback** — stored on the task; consumed once a run reaches a terminal state; a re-queued
|
||||||
|
task resumes the session or folds feedback into the prompt.
|
||||||
|
- **Child tasks** — planning creates draft children; finalization requires no Queued children remain;
|
||||||
|
OrphanRecovery dequeues children if the parent is not planning.
|
||||||
|
- **Lifecycle recovery** runs at startup: stale-Running → Failed; orphaned children → dequeued but attached.
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# Fix-Plan — Verifikations-Findings (Stand 2026-07-24)
|
||||||
|
|
||||||
|
Einstiegspunkt für eine **frische Fix-Session**. Sammelt die in der manuellen Verifikation
|
||||||
|
(§7–§9 + Kanten §1/§3/§4) gefundenen Probleme, gruppiert nach **Fixbarkeit**. Volltext je
|
||||||
|
Finding (mit Kontext/Wiederholschritten) steht in `docs/open.md`; hier steht der Fix-Blick:
|
||||||
|
Root-Cause, konkreter Ansatz, Loc/Test-Hinweise, und **welche Punkte vor der Umsetzung eine
|
||||||
|
Entscheidung brauchen**.
|
||||||
|
|
||||||
|
**Immer zuerst:** file:line-Angaben gegen den aktuellen Code prüfen (können minimal driften).
|
||||||
|
Build/Test-Regeln + Gotchas s. Projekt-`CLAUDE.md` (u.a. `.slnx` braucht .NET 9 → einzelne
|
||||||
|
`.csproj -c Release`; Localization.Tests erzwingt en/de-Parität; Subagents `sonnet`, Dateien
|
||||||
|
pfad-scoped stagen). Pro Fix ein Conventional Commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bearbeitungsstand (Session 2026-07-24, nicht gepusht)
|
||||||
|
|
||||||
|
**Erledigt & committed:**
|
||||||
|
- Gruppe A #1–5 + beide Optional-Nits (Icon.Plus gefüllt, Gear-PathIcon, Skills-Empty-State,
|
||||||
|
„Subtasks"-Terminologie, Conflict-Continue-Hinweis, Rename-Darstellung, Turns/Tokens-Reload).
|
||||||
|
- Gruppe B #7 (Resume-Fehler surfacen) + #8 (Attachment-Drop-Diagnose).
|
||||||
|
- Gruppe C #9 (AskUser-Banner in Detail-Insel via geteiltem `TaskMonitorViewModel`),
|
||||||
|
#11 (OUTCOME zeigt `summary` statt rohem JSON: Worker-Unwrap + UI-Sicherheitsnetz).
|
||||||
|
- Gruppe D #13 (Kind-Rows live-refresh bei Parent-Planning-Transitionen) + #14 (Idle-Chip auf
|
||||||
|
Planning-Parents ausgeblendet). *Visual-Verification für #13 (Finalize/Discard live) offen.*
|
||||||
|
|
||||||
|
**#6** war im aktuellen Code bereits abgedeckt (Worker wirft `HubException` bei `blocked`
|
||||||
|
→ UI-Dialog); zusätzlich als ClaudeDo-Task `f9809a93` erfasst. Nicht angefasst.
|
||||||
|
|
||||||
|
**Offen:**
|
||||||
|
- **#10 + #12** — bewusst gebündelt mit dem ConPTY-Planning-Task `5d627df8` (dort lässt sich
|
||||||
|
die Session-Id sauber greifen bzw. das MCP-Permission-Verhalten klären). Sofort-Schutz für
|
||||||
|
#10 (Resume ausgrauen wenn keine Id) wurde NICHT gebaut — bräuchte Worker-Plumbing, das der
|
||||||
|
ConPTY-Umbau ohnehin liefert; der #7-Fix verhindert bereits das stille Scheitern.
|
||||||
|
- **Gruppe E** — nur noch design-/feature-behaftete Punkte. Der scheinbare Quick-Win
|
||||||
|
„Dequeue-X auf blockierten Kettengliedern" wurde bewusst NICHT umgesetzt: einzelnes Dequeue
|
||||||
|
eines Kettenglieds hinterlässt hängende Nachfolger (deren `BlockedByTaskId` zeigt weiter auf
|
||||||
|
das nun idle Glied) → braucht Chain-Repair-Design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gruppe A — Mechanisch, sofort fixbar (keine Entscheidung nötig)
|
||||||
|
|
||||||
|
Ideale Kandidaten für den Start / parallele Subagents (disjunkte Dateien).
|
||||||
|
|
||||||
|
1. **„New session"-Button unsichtbar (Icon.Plus strich-only)**
|
||||||
|
`IslandStyles.axaml` (`Icon.Plus` = `M12 5v14M5 12h14`, reine Linie) wird in einem
|
||||||
|
`<PathIcon>` (MissionControlView.axaml, füllt Geometrie) unsichtbar gerendert.
|
||||||
|
**Fix:** `Icon.Plus` als gefüllte Geometrie authoren ODER als gestricheltes `Path`
|
||||||
|
rendern (vgl. `Path.plan-icon`). **Andere `Icon.Plus`-Verwendungen mitprüfen.**
|
||||||
|
|
||||||
|
2. **Agent-Settings-Gear weicht vom Listen-Gear ab**
|
||||||
|
`TaskHeaderBar.axaml:68` rendert `<TextBlock Text="⚙">`; überall sonst `Icon.Settings`
|
||||||
|
(PathIcon, IslandStyles.axaml:110). **Fix:** den `⚙`-TextBlock durch
|
||||||
|
`<PathIcon Data="{StaticResource Icon.Settings}" Width=".." Height=".."/>` ersetzen.
|
||||||
|
|
||||||
|
3. **Session-Skills-Tab ohne Empty-State**
|
||||||
|
Bei 0 Skills nur nackte Fläche. **Fix:** Empty-State-Text unter der Install-Zeile
|
||||||
|
(z.B. „No skills installed — paste a GitHub URL above"). **Loc:** neue Keys in en.json
|
||||||
|
**und** de.json (Parität!). Datei: `SessionSkillsSettingsTab*`.
|
||||||
|
|
||||||
|
4. **„Waiting for Improvements" für Planning-Parents (Terminologie)**
|
||||||
|
`en.json` `taskStatus.waitingForChildren`/`agentStatus.children`/`childOutcomesLabel`
|
||||||
|
sagen „Improvements". Seit unified-parent gilt `WaitingForChildren` auch für Planning.
|
||||||
|
**Fix:** auf neutrales „Waiting for Subtasks"/„Subtasks"/„SUBTASKS" umstellen — en **und**
|
||||||
|
de (Parität).
|
||||||
|
|
||||||
|
5. **Conflict-Resolver: Continue-Button klickbar trotz offener Konflikte**
|
||||||
|
Merge passiert korrekt erst nach Auflösung, aber der Button ist nicht disabled → früher
|
||||||
|
Klick = stummer No-op. **Fix:** `CanContinue`/`AllResolved` an `IsEnabled` binden (ggf.
|
||||||
|
Hinweis „N Konflikte in M Dateien offen"). Datei: `ConflictResolverView(.axaml)` +
|
||||||
|
`ConflictResolverViewModel`.
|
||||||
|
|
||||||
|
Optional-Nits (gleiche Gruppe, niedrige Prio):
|
||||||
|
- **Diff-Viewer Rename schwach dargestellt** (alt→neu-Pfad + „renamed"-Label statt „+0 −0").
|
||||||
|
- **Header-TurnsText `0/max`** bei terminalem Reload — `Turns` aus `task_runs.turnCount`
|
||||||
|
restaurieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gruppe B — Error-Surfacing (klare Richtung: kein stiller/leerer Fehlerpfad)
|
||||||
|
|
||||||
|
Leitlinie `feedback_ui_error_surfacing`: User-Action-Fehler in den Footer
|
||||||
|
(`FlashFooterError`) bzw. Dialog, nie leerer `catch`/stiller No-op.
|
||||||
|
|
||||||
|
6. **Approve & Merge schluckt „blocked" still**
|
||||||
|
`DetailsIslandViewModel.ApproveReviewAsync` reagiert nur auf `Status == "conflict"`; bei
|
||||||
|
`"blocked"` (z.B. dirty Ziel-Tree) passiert nichts. **Fix:** bei `blocked`/unerwartetem
|
||||||
|
Status `result.ErrorMessage` surfacen. *(Als ClaudeDo-Task `f9809a93` erfasst — koppelt
|
||||||
|
„Approve erzwingt Diff/Review vor Merge".)*
|
||||||
|
|
||||||
|
7. **„Resume planning session" verschluckt den Fehler (Teil-Fix hier, Rest → Gruppe C #10)**
|
||||||
|
`TasksIslandViewModel.ResumePlanningSessionAsync` (~Zeile 870) hüllt alles in `catch { }`.
|
||||||
|
**Sofort-Fix:** den Fehler surfacen statt schlucken. Der eigentliche Resume-Defekt braucht
|
||||||
|
eine Entscheidung → #10.
|
||||||
|
|
||||||
|
8. **Attachments: intermittenter erster-Drop-Fehler („An error occurred")**
|
||||||
|
Einmalig beobachtet (erster Drop der Session, nichts persistiert), nicht reproduzierbar.
|
||||||
|
**Fix (diagnostisch):** `AddFilesAsync`/`OnDrop` robustes Error-Logging geben (die genaue
|
||||||
|
Exception fehlt, weil `DropStatus` nur `{fileName}: {ex.Message}` zeigt) — damit der
|
||||||
|
nächste Fall auswertbar ist. Kandidaten-Ursachen: SQLite-Contention (UI schreibt `todo.db`
|
||||||
|
direkt, während der Worker sie hält) oder Drop-Stream-Pfad (`IStorageFile.OpenReadAsync`
|
||||||
|
im Code-Behind, außerhalb des `try`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gruppe C — Erst Entscheidung/Brainstorm, DANN umsetzen (nicht blind fixen)
|
||||||
|
|
||||||
|
9. **AskUser-Interaktion auch in der Detail-Insel** *(von Mika ausdrücklich gewünscht)*
|
||||||
|
Der `ask_user`-Banner + Inline-Antwort existiert nur in Mission Control
|
||||||
|
(`MonitorPaneView`); die Detail-Insel zeigt für den laufenden Task nichts.
|
||||||
|
**Entscheidung:** wie den Zustand teilen — `TaskMonitorViewModel` hält ihn bereits; für
|
||||||
|
die Detail-Insel replizieren, teilen, oder ein gemeinsames Banner-Control? Danach:
|
||||||
|
Banner + `AnswerDraft`/`SubmitAnswer` in `DetailsIslandView(Model)` einhängen.
|
||||||
|
|
||||||
|
10. **„Resume planning session" grundsätzlich kaputt (Session-Id nie erfasst)**
|
||||||
|
`PlanningSessionManager.ResumeAsync:238` wirft immer „No Claude session ID captured yet",
|
||||||
|
weil `TaskRepository.UpdatePlanningSessionIdAsync:322` **keinen Aufrufer** hat →
|
||||||
|
`planning_session_id` bleibt NULL. **Entscheidung:** (a) claude-Session-Id der
|
||||||
|
wt-Planning-Session erfassen + via `UpdatePlanningSessionIdAsync` persistieren (echtes
|
||||||
|
Resume) ODER (b) Resume entfernen/deaktivieren, wenn keine Id vorliegt. Hängt mit der
|
||||||
|
Design-Entscheidung „Planning über embedded ConPTY statt wt" zusammen (ClaudeDo-Task
|
||||||
|
`5d627df8`) — dort ließe sich die Session-Id sauber greifen.
|
||||||
|
|
||||||
|
11. **OUTCOME-Karte rendert rohes Structured-Output-JSON**
|
||||||
|
`TaskMonitorViewModel.ApplyOutcome` setzt bei Tasks ohne Roadblock `SessionOutcome`
|
||||||
|
= `task.Result` wörtlich; der Worker legt dort rohes `{"summary":…}` ab.
|
||||||
|
**Entscheidung:** (a) UI parst JSON-Result und zeigt `summary`, oder (b) Worker schreibt
|
||||||
|
`summary`/`resultMarkdown` statt JSON in `task.Result`.
|
||||||
|
|
||||||
|
12. **Planning-Session prompted nach MCP-Tool-Permission**
|
||||||
|
Trotz `--allowedTools "mcp__claudedo__*,…"` + `--permission-mode plan` prompted die
|
||||||
|
wt-Planning-Session beim ersten `create_child_task`. **Untersuchen/Entscheiden:** matcht
|
||||||
|
der `mcp__claudedo__*`-Glob in CLI 2.1.207 nicht (Syntax evtl. ganzer Server-Name), oder
|
||||||
|
gated Plan-Mode MCP-Writes generell? Gekoppelt an ConPTY-Planning-Task `5d627df8`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gruppe D — Erst Root-Cause pinnen (Investigation)
|
||||||
|
|
||||||
|
13. **Kind-Rows aktualisieren nach Parent-Planning-Transitionen nicht live**
|
||||||
|
Nach **Finalize** bleiben Kind-Badges „Draft" statt „Planned"; nach **Discard** bleiben
|
||||||
|
die (in der DB gelöschten) Kind-Rows sichtbar — bis Listen-Reload. Doppelt verifiziert §3.
|
||||||
|
**Untersuchen:** wie wird die Kinderliste/-gruppierung auf ein Parent-`TaskUpdated`
|
||||||
|
reagierend neu aufgelöst? Vermutlich fehlt ein Regroup/Refetch der Children beim
|
||||||
|
Parent-Broadcast (`TasksIslandViewModel` hierarchie-Regrouping). Fix danach: Children bei
|
||||||
|
Parent-Transition live neu auflösen.
|
||||||
|
|
||||||
|
14. **Planning-aktiver Parent zeigt weiter „Idle"**
|
||||||
|
Parent `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber der Row-Chip
|
||||||
|
zeigt „Idle"; `PlanningBadge` überschreibt das nicht sichtbar. **Untersuchen/Design:** ein
|
||||||
|
klarer „Planning/Draft aktiv"-Zustand, der den Idle-Chip überschreibt. (Verwandt mit #13 —
|
||||||
|
Row-Statusdarstellung.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gruppe E — UX-Nits / Feature-Wünsche (niedrige Prio, sammeln)
|
||||||
|
|
||||||
|
- **Conflict-Resolver: mehrere Konfliktdateien schlecht erkennbar** — prominentere Datei-Liste
|
||||||
|
/ „x von y Dateien".
|
||||||
|
- **Blocked-by-Kette nicht visualisiert** — Reihenfolge/Abhängigkeit darstellen
|
||||||
|
(„wartet auf <Vorgänger>").
|
||||||
|
- **Dequeue-„X" fehlt auf blockierten Kettengliedern** — `CanRemoveFromQueue` erweitern
|
||||||
|
(`IsWaiting` einschließen).
|
||||||
|
- **„Open ConPTY session" erneut = Prompt wird neu gesendet** — Resume-Affordance / Re-Open-
|
||||||
|
Warnung (bewusst kein Session-Persist).
|
||||||
|
- **Conflict-Resolver: farbliches Hervorheben übernommener Zeilen im Result-Pane** (Feature).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht anfassen / Kontext
|
||||||
|
|
||||||
|
- **§1 DiffModal-Fehler-State** (`vm.diff.unavailable`) ist **defensiver, über die UI
|
||||||
|
unerreichbarer** Code — alle Aufrufer sind gegated (`CanDiffMergedRange` verlangt base+head
|
||||||
|
non-null; `ConfigureWorktree` nur mit existierendem Pfad). Kein Fix nötig.
|
||||||
|
- **`--permission-mode auto` + `haiku` denied Writes** — modellabhängiges Verhalten, keine
|
||||||
|
Regression; Default (sonnet) unbetroffen. Beobachten (Memory `auto_permission_haiku_footgun`).
|
||||||
|
- **§10 Daily Prep/Weekly** — Verifikation zurückgestellt bis zum geplanten Rework.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
|
1. **Gruppe A** (mechanisch, schnell, teils parallel) → sofort sichtbare Wins.
|
||||||
|
2. **Gruppe B** (Error-Surfacing, klein & risikoarm).
|
||||||
|
3. **Gruppe C** — pro Punkt kurz brainstormen/entscheiden, dann umsetzen (#10 + #12 zusammen
|
||||||
|
mit der ConPTY-Planning-Entscheidung betrachten).
|
||||||
|
4. **Gruppe D** — Investigation, dann Fix (#13 zuerst — betrifft mehrere Planning-Flows).
|
||||||
|
5. **Gruppe E** — nach Bedarf.
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Handoff — List-handler run on list "Claude do", 2026-08-05
|
||||||
|
|
||||||
|
> **✅ COMPLETED 2026-08-05 (follow-up session).** Everything in §1 and §2 is merged and `main`
|
||||||
|
> verified green. Read §0 below before §1–§8 — §5's diagnosis turned out to be **wrong** and the
|
||||||
|
> rest is now history. Still nothing pushed.
|
||||||
|
|
||||||
|
Repo: `C:\Private\ClaudeDo` · List id: `5f973815-050a-4136-94f0-1506a5d4560a` · Branch: `main` (nothing pushed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. What the follow-up session did (and what §5 got wrong)
|
||||||
|
|
||||||
|
**All merged, `main` green after every step** (Worker 811/811, Data 143/143, Ui 292/292,
|
||||||
|
Localization 16/16, builds 0 new warnings):
|
||||||
|
|
||||||
|
| Merged | Task | Merge commit |
|
||||||
|
|---|---|---|
|
||||||
|
| §1 #1 | `9e307199` revert_merge + merge-SHA persistence | `3e7126b` |
|
||||||
|
| §1 #2 | `0b2fbb48` post-merge verification gate | (conflict-resolved) |
|
||||||
|
| §1 #3 | `8c1c2130` roadblock reply box | `519ea5a` |
|
||||||
|
| §2 #42 | `c1df5b9a` Hub surface for usage | `6e2158d` |
|
||||||
|
| §2 #43 | `f74b44d9` Usage pill | `7661129` |
|
||||||
|
| §2 #45 | `06068810` gate thresholds in settings | `5115cfc` |
|
||||||
|
| §2 #44 | `82488d2a` Usage Monitor modal | `338fc39` |
|
||||||
|
| §2 #46 | `9c8cffe0` docs | `5872666` |
|
||||||
|
| parent | `439a4daf` Usage Monitor unit | approved → Done (empty unit merge; all children already `Merged`) |
|
||||||
|
|
||||||
|
Plus one hand-fix on `main`: `677a4c1` — `UsagePillViewModelTests` never set `Loc.Current`
|
||||||
|
(defaults to a key-echo localizer) and only passed because another test class happened to
|
||||||
|
install a real `Localizer` first; #44's new tests changed the ordering and broke it on `main`.
|
||||||
|
Classic "both branches green, `main` red".
|
||||||
|
|
||||||
|
### §5 is wrong — `"exited with code 1 and no result"` is NOT (only) a CLI crash
|
||||||
|
|
||||||
|
It is a **catch-all** hiding at least three causes. The truth is in the run log's last NDJSON
|
||||||
|
line (`{"type":"result", …}` → `terminal_reason` / `errors` / `result`):
|
||||||
|
|
||||||
|
- **`max_turns`** — what actually killed #40 and #42. `app_settings.model_presets` is `NULL`, so
|
||||||
|
`ModelPresets.Parse` falls back to the shipping defaults, and **sonnet's default is 30 turns**.
|
||||||
|
`AppSettings.DefaultMaxTurns` (100) is only the fallback for an *unrecognized* alias, so it
|
||||||
|
never applies. Every task without an explicit `maxTurns` override got 30 turns.
|
||||||
|
→ Fix used here: `set_task_config(taskId, model="sonnet", maxTurns=200)` before queueing.
|
||||||
|
- **`api_error`** + `"You've hit your session limit · resets 1pm (Europe/Berlin)"` — the account's
|
||||||
|
5-hour limit, which took out #43's first attempt. Nothing to fix; wait for the reset, re-queue.
|
||||||
|
- Genuine process death — the case §5 describes.
|
||||||
|
|
||||||
|
§5's *operational* advice still holds and is what saved #42: **never `reset_failed_task`** on one
|
||||||
|
of these; check the worktree, build/test it, then set the task `Queued` so the agent resumes its
|
||||||
|
own session and commits. #42's worktree held a complete green implementation (773/773).
|
||||||
|
|
||||||
|
Follow-up tasks: `ca6e55c0` (surface the real failure reason) is new; the turn-budget/presets side
|
||||||
|
is already covered by the Idle task `2de2f008` (`b0317ec7` fixed only the unknown-alias half).
|
||||||
|
|
||||||
|
### Still open
|
||||||
|
|
||||||
|
- **Visual passes** (nobody has looked at these in a running app): usage pill in the footer *and*
|
||||||
|
the Mission Control header; Usage Monitor modal (gauges, tables, stale/blocked bands, dark/light);
|
||||||
|
the roadblock reply box; the verify-command field in the List Settings modal. See `docs/open.md`.
|
||||||
|
- **`~/.todo-app/prompts/planning.md` still shadows the compiled default** (§7.1) — unchanged.
|
||||||
|
- **`wait_for_task_change` is merged but not in the running Worker**, so it isn't callable over MCP
|
||||||
|
until the Worker is restarted. §3's sqlite poll was used instead.
|
||||||
|
- One rough edge in the verify gate: if the verify command fails, the worktree has already been
|
||||||
|
removed and its state set `Merged` while the task stays `WaitingForReview` — re-approving is then
|
||||||
|
refused. Recovery is `update_task_status(..., "Done")` once `main` is fixed.
|
||||||
|
|
||||||
|
Predecessor session ran the five-phase list handler over 11 briefed tasks and, along the way,
|
||||||
|
absorbed the 9-child "Usage Monitor" unit. **Phases 0–3 are complete for the brief.** What is
|
||||||
|
left is Phase 4 (review + merge) for three tasks, plus the Usage chain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Do this first — three brief tasks sit in WaitingForReview
|
||||||
|
|
||||||
|
Merge in **this order** (the order was chosen with the user and matters):
|
||||||
|
|
||||||
|
| # | Task | Id | Note |
|
||||||
|
|---|------|----|------|
|
||||||
|
| 1 | Feat: Merge zurücknehmen — Merge-Commit festhalten + `revert_merge` | `9e307199-2eca-4eb7-9057-d2c675cc57ca` | Migration + new tool. Merge **before** #2 |
|
||||||
|
| 2 | Feat: Verifikations-Gate nach dem Merge | `0b2fbb48-d44c-4155-8c21-d3464c0bd5c2` | Depends on #1's merge-SHA persistence; both edit `TaskMergeService.cs` |
|
||||||
|
| 3 | Feat: Antwortfeld auf der Roadblock-Karte | `8c1c213004574c4fad6beb75b84b70d7` | UI + localization (en **and** de) |
|
||||||
|
|
||||||
|
For each one:
|
||||||
|
|
||||||
|
1. `get_task_diff(taskId, stat=true)`, then the full diff if non-trivial. Sanity-check against
|
||||||
|
the task description (they are long and precise — the acceptance criteria are the checklist).
|
||||||
|
2. `review_task(taskId, decision="approve", leaveConflictsInTree=true)`.
|
||||||
|
3. On conflict: open the files under the returned `repoPath`, resolve keeping **both** sides'
|
||||||
|
intent, then `continue_merge(taskId)`. Conflicts are expected and normal here.
|
||||||
|
4. **After every merge, verify `main`** (see §4). This is non-negotiable — see §5.
|
||||||
|
|
||||||
|
Expected conflicts: `TaskMergeService.cs` between #1 and #2; `src/ClaudeDo.Worker/CLAUDE.md`
|
||||||
|
and `src/ClaudeDo.Data/CLAUDE.md` in nearly every merge (doc bullet lists — trivial, keep both
|
||||||
|
sides' entries).
|
||||||
|
|
||||||
|
## 2. Then the Usage Monitor unit
|
||||||
|
|
||||||
|
Parent `439a4daf166f4ab5b0fa415693d2c80d` ("Usage Monitor hinzufügen") is `WaitingForChildren`
|
||||||
|
and has **no worktree of its own**. It has 9 children. Four are merged, one is in flight, four
|
||||||
|
are Idle.
|
||||||
|
|
||||||
|
| Child | Id | State |
|
||||||
|
|---|---|---|
|
||||||
|
| #38 Data: Usage-Gate-Schwellen + Modell-Spalte | `c1c999b6-b800-4b6b-a821-fbc028c15772` | merged `b1efcdc` |
|
||||||
|
| #39 Worker: OAuth-Usage-Client + Poller | `f657e316-ad72-4f45-8036-460841fc8997` | merged `b126a21` |
|
||||||
|
| #40 Worker: UsageGate | `06a7cc32-6ab7-4758-98f4-bee77149b2bf` | merged `1ee21b5` |
|
||||||
|
| #41 Worker: TranscriptUsageReader | `840fdb98-1c0e-4219-8062-c8769233fc14` | merged `334cf1e` |
|
||||||
|
| #42 Worker: Hub-Surface für Usage | `c1df5b9a-b911-4fe8-aab4-5876d9d85793` | **re-queued, in flight — read §5 before touching** |
|
||||||
|
| #43 UI: Usage-Pill | `f74b44d9-7e48-4bfe-9d89-075e194d1fc9` | Idle — queue once #42 is merged |
|
||||||
|
| #45 UI: Gate-Schwellen im Settings-Modal | `06068810-5b5c-4635-80dd-62eeba89fb8c` | Idle — queue once #42 is merged (parallel with #43) |
|
||||||
|
| #44 UI: Usage-Monitor-Modal | `82488d2a-8ff7-41b8-b791-367959a8f827` | Idle — needs #42 **and** #43 merged |
|
||||||
|
| #46 Docs: Usage Monitor | `9c8cffe0-8f7b-401e-a4f0-33b937047082` | Idle — last, after everything is merged |
|
||||||
|
|
||||||
|
**The chain is strictly serial and you must respect it.** Every child forks from `main`, and each
|
||||||
|
one's own description hard-requires the earlier ones. Queueing them all at once is exactly what
|
||||||
|
produced the original roadblock: #40 ran, found its prerequisite types only on unmerged sibling
|
||||||
|
branches, and returned `Done` having written **zero** code. So: merge a child → then queue the
|
||||||
|
next → verify `main` → repeat.
|
||||||
|
|
||||||
|
When the last child is merged the parent surfaces for review by itself; approve it to close the
|
||||||
|
unit (it has no worktree, so it approves straight to Done).
|
||||||
|
|
||||||
|
## 3. Cheap status polling — important
|
||||||
|
|
||||||
|
`list_tasks` and `batch_get_tasks` return full descriptions and **blow the token limit** on this
|
||||||
|
list (`list_tasks` over 52 tasks = ~206,000 chars; that is literally one of the bugs this run
|
||||||
|
fixed). Do not poll with them. Poll the DB read-only instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONIOENCODING=utf-8 python - <<'EOF'
|
||||||
|
import sqlite3
|
||||||
|
c=sqlite3.connect("file:C:/Users/mika.kuns/.todo-app/todo.db?mode=ro",uri=True)
|
||||||
|
for i,s in c.execute("select id,status from tasks"):
|
||||||
|
print(s, i)
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
`list_worktrees` is also compact and safe. **New this run:** `wait_for_task_change(taskIds,
|
||||||
|
timeoutSeconds)` is now merged and is the proper primitive — it returns as soon as any listed
|
||||||
|
task leaves Queued/Running (server-clamped to 170 s). Prefer it over sleeping.
|
||||||
|
|
||||||
|
## 4. Verify main after every merge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
For the UI/localization task (#3 above, and children #43–#45) also:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9 — build individual csproj files, `-c Release` (a running Worker locks Debug).
|
||||||
|
|
||||||
|
Baseline as of this handoff: Worker **753/753**, Data **143/143**, build 0 warnings.
|
||||||
|
|
||||||
|
## 5. The trap that cost this run the most time
|
||||||
|
|
||||||
|
Two children "failed" with `"Claude exited with code 1 and no result"`. **That is a CLI crash,
|
||||||
|
not bad code.** In both cases the worktree held complete work that built with 0 warnings and
|
||||||
|
passed the full suite (#40: 732/732, #42: 766/766) — the run just died before the auto-commit.
|
||||||
|
|
||||||
|
- **Never `reset_failed_task` on such a task** — it discards the worktree and destroys the work.
|
||||||
|
- Instead: `cd` into the worktree, `git status`, build + test it. If green, set the task
|
||||||
|
`Queued`. The worktree is preserved and the agent resumes its own session (`--resume`),
|
||||||
|
finds its work and commits it. That is how #40 was recovered.
|
||||||
|
- #42 is mid-recovery right now via exactly this route. If it failed again, verify its worktree
|
||||||
|
(`C:\Private\.claudedo-worktrees\claude-do\c1df5b9a-b911-4fe8-aab4-5876d9d85793`) before
|
||||||
|
doing anything destructive.
|
||||||
|
|
||||||
|
Second trap: git merges cleanly and the **compiler** still breaks. It happened again this run —
|
||||||
|
`Usage/UsageModels.cs` was an add/add conflict, and `src/ClaudeDo.Worker/CLAUDE.md` merged
|
||||||
|
"cleanly" into a file with the `Usage/` folder documented **twice**. Always read what a clean
|
||||||
|
merge produced, and always run §4.
|
||||||
|
|
||||||
|
## 6. Phase 0–3 decisions already made (do not redo)
|
||||||
|
|
||||||
|
Dedupe: four candidate pairs examined, **nothing cancelled**. Decisions:
|
||||||
|
|
||||||
|
- `05827da5` ↔ `81e37801` — kept both, and `05827da5` was **re-scoped**: its "lean status query"
|
||||||
|
half was removed because `81e37801`'s wait tool covers it. `05827da5` now owns only the
|
||||||
|
brief-description rendering. Both are merged.
|
||||||
|
- `a76d9547` ↔ `99732497` — kept both, `99732497` merged first. Done.
|
||||||
|
- `0b2fbb48` ↔ `9e307199` — kept both, `9e307199` merges first. **This is item #1/#2 in §1.**
|
||||||
|
- `20c78c95` ↔ `a76d9547`(b) — kept both, different actors. Done.
|
||||||
|
|
||||||
|
Phase 2: all 11 tasks carry acceptance criteria, real file+line references and out-of-scope
|
||||||
|
sections. Three that were one-liners were researched and rewritten after asking the user
|
||||||
|
(ConPTY fix approach, maxTurns-only scope, roadblock reply-box design).
|
||||||
|
|
||||||
|
Run config: `maxParallelExecutions` = **3**. No list config exists, so effective max turns was
|
||||||
|
the global **100**; it was raised to **200** per-task on the five heaviest via `set_task_config`.
|
||||||
|
`0b2fbb48`, `9e307199` and `8c1c2130` still carry that override.
|
||||||
|
|
||||||
|
## 7. Open follow-ups worth new tasks
|
||||||
|
|
||||||
|
1. **`~/.todo-app/prompts/planning.md` shadows the planning prompt.** `PromptFiles.EnsureExists`
|
||||||
|
only writes a default when the file is absent, and that file exists (dated Jun 2). The
|
||||||
|
`maxTurns` guidance merged in `65db1cd` therefore **does not reach real planning sessions**
|
||||||
|
until that file is updated by hand. `system.md` and `agent.md` are shadowed too.
|
||||||
|
`merge-helper-system.md`/`merge-helper-initial.md` do **not** exist, so this run's handler
|
||||||
|
prompt changes are live.
|
||||||
|
2. **MCP task DTOs expose no parent/child link.** The 9-child Usage unit had to be reconstructed
|
||||||
|
from `sortOrder` and creation timestamps. `get_task`/`list_tasks` should return
|
||||||
|
`parentTaskId` / `blockedByTaskId`.
|
||||||
|
3. **Visual verification open** on: the ConPTY fix (open a tile on a task whose description
|
||||||
|
contains `->`), and — once merged — the roadblock reply box and the verify-gate field in the
|
||||||
|
list settings modal.
|
||||||
|
4. **`"exited with code 1 and no result"` is too common.** Three runs died that way today, two
|
||||||
|
with finished work. Worth investigating whether the auto-commit step can be made to survive
|
||||||
|
a late CLI crash.
|
||||||
|
5. Nothing has been **pushed**. `main` is 22 commits ahead of `8d7ba1e`.
|
||||||
|
|
||||||
|
## 8. Rules this session operated under
|
||||||
|
|
||||||
|
- Drive merges through the MCP tools. Never raw `git merge` / `reset` / `checkout`.
|
||||||
|
- Hand-resolve only markers the tools left behind, then `continue_merge`. When committing by
|
||||||
|
hand is unavoidable, stage **explicit paths** — never `git add -A`: the main checkout is
|
||||||
|
shared with other sessions.
|
||||||
|
- For a parent/children unit merge, pass the **parent** id to `continue_merge` / `abort_merge`.
|
||||||
|
- Ask the user on anything ambiguous, risky, or destructive.
|
||||||
|
- Never `delete_task` to dedupe — `Cancelled` keeps it visible and resettable.
|
||||||
+97
-42
@@ -1,64 +1,119 @@
|
|||||||
# ClaudeDo — Offene Punkte
|
# ClaudeDo — Offene Punkte
|
||||||
|
|
||||||
Stand: 2026-06-10. **Nur noch offene Punkte.** Was erledigt ist, steht in den Commits und im Code — nicht hier.
|
Stand: 2026-07-24. Diese Datei listet die **aktiv verifizierten Findings** aus der Verifikations-Session vom 2026-07-24. Die laufende Verifikations-Checkliste (Pass/Fail je Abschnitt, inkl. noch offener Abschnitte §7–§11 und Kanten) lebt in `docs/verification-handoff.md`. Erledigtes steht in den Commits/im Code, nicht hier.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manuelle Verifikation (offen)
|
## Bugs (verifiziert 2026-07-24)
|
||||||
|
|
||||||
Kein Code-Aufwand, nur Durchspielen mit explizit notiertem Pass-Kriterium. Der Großteil der Pipeline ist laut User bereits in der Praxis getestet; hier das, was noch ein falsifizierbares Observable braucht.
|
- **OUTCOME-Karte rendert rohes Structured-Output-JSON:** `TaskMonitorViewModel.ApplyOutcome` (ClaudeDo.Ui) setzt bei Tasks ohne Roadblock-Marker `SessionOutcome = result` wörtlich (Zeile ~261). Der Worker legt in `task.Result` das rohe `{"summary":…,"files_changed":[…]}` ab (die lesbare Fassung steht in `task_runs.resultMarkdown`), also zeigt die Detail-Insel OUTCOME als JSON-Blob statt als Text. Fix: (a) UI parst ein JSON-Result und zeigt `summary`, oder (b) Worker schreibt `summary`/`resultMarkdown` statt des JSON in `task.Result`. Verifiziert am Task `verif §1 diff matrix`. Deckt sich mit Memory `worker_testing_findings`.
|
||||||
|
- **Approve & Merge schluckt einen „blocked"-Merge still:** `DetailsIslandViewModel.ApproveReviewAsync` reagiert nur auf `result.Status == "conflict"` (öffnet den Resolver); bei `"blocked"` (z.B. Ziel-Working-Tree hat uncommittete getrackte Änderungen) und anderen Nicht-`merged`/Nicht-`conflict`-Status passiert **nichts** — kein Footer-Fehler, kein Dialog (der `catch` greift nur bei Exceptions, „blocked" ist aber ein normaler Rückgabewert mit `ErrorMessage`). User sieht „Klick tut nichts". Doppelt verifiziert (Konflikt-Approve UND sauberer additiver Approve `verif §1b`, beide bei dirty `main`-Checkout still). Fix: bei `blocked`/unerwartetem Status `result.ErrorMessage` via `ShowErrorAsync`/`FlashFooterError` surfacen. Verstößt gegen `feedback_ui_error_surfacing`. (Der Konflikt-Resolver + 3-Pane-Editor funktionieren, sobald der Ziel-Tree sauber ist.)
|
||||||
|
- **„Waiting for Improvements" für Planning-Parents (Terminologie):** `taskStatus.waitingForChildren` = „Waiting for Improvements" (en.json:504), `agentStatus.children` dito (503), `childOutcomesLabel` = „IMPROVEMENTS" (191). Seit dem unified-parent-Modell gilt `WaitingForChildren` für Planning **und** Improvement-Parents — „Improvements" ist für einen Planning-Parent falsch. Auf neutrales „Waiting for Subtasks"/„Subtasks" umstellen (en **und** de — Localization.Tests-Parität). Verifiziert §3.
|
||||||
|
- **Kind-Rows aktualisieren nach Parent-Planning-Transitionen nicht live:** Parent-getriebene Änderungen propagieren nicht auf die Kind-`TaskRowViewModel`s ohne Listen-Reload. (a) Nach „Finalize planning" bleiben Kind-Rows auf „Draft" (`IsDraft`) statt „Planned" (`IsPlanned`) — der geänderte `ParentFinalized` kommt nicht über das Parent-`TaskUpdated`-Broadcast an. (b) Nach „Discard" bleiben die (in der DB gelöschten) Draft-Kind-Rows sichtbar, bis die Liste neu geladen wird. Gemeinsame Ursache: die Kinderliste/-zustände werden bei Parent-Transitionen nicht live neu aufgelöst. Doppelt verifiziert §3 (Finalize **und** Discard, 2026-07-24).
|
||||||
|
- **AskUser-Frage erscheint NUR in Mission Control, nicht in der Task-Detail-Insel:** Der `ask_user`-Banner (Frage + Antwort-Eingabe) lebt ausschließlich in `MonitorPaneView` (Mission Control). Die Detail-Insel (`DetailsIslandView`) streamt zwar den Live-Log eines laufenden Tasks, zeigt aber **kein** Frage-Banner — ein Nutzer, der nur die Detailansicht offen hat, sieht nicht, dass der Run auf eine Antwort wartet, und läuft nach 3 min in den Timeout-Fallback. Verifiziert §7 (User schaute Detailansicht, Frage war unsichtbar; erst in Mission Control sichtbar). Fix: Frage-Banner + Inline-Antwort auch in der Detail-Insel für den gebundenen laufenden Task surfacen (VM-Zustand liegt bereits in `TaskMonitorViewModel`, müsste für die Detail-Insel repliziert/geteilt werden). **Von Mika bei der §7-Verifikation ausdrücklich gewünscht** („vermisse diese Interaktion in der Detail-Ansicht"). §7.
|
||||||
|
- **„New session"-Button (Mission Control) unsichtbar — Icon.Plus ist Strich-Only:** `Icon.Plus` = `M12 5v14M5 12h14` (IslandStyles.axaml:61) ist reine Linien-Geometrie ohne Fläche; im `<PathIcon>` (MissionControlView.axaml:38, füllt Geometrie) rendert sie **unsichtbar** → der Ad-hoc-„New session"-Button erscheint leer und ist nicht auffindbar (Feature + VM `OpenAdHocConPtySessionAsync` + Tests funktionieren, nur das Icon fehlt sichtbar). Fix: Icon.Plus als gefüllte Geometrie authoren ODER als gestricheltes `Path` rendern (Icon-Gotcha in CLAUDE.md). **Andere `Icon.Plus`-Verwendungen mitprüfen.** Verifiziert §5.
|
||||||
|
|
||||||
- **Worktree-Pipeline:**
|
## UX / Nits (verifiziert 2026-07-24)
|
||||||
- Worktree-Happy-Path → `worktrees.state='active'`, `head_commit` gesetzt, `diff_stat` non-empty, Branch `claudedo/<id>` auf Disk.
|
|
||||||
- No-Changes-Run → `status='Done'`, `head_commit IS NULL`, `diff_stat IS NULL`.
|
|
||||||
- Kein Git-Repo (`working_dir=C:\Temp`) → `status='Failed'`, **keine** `worktrees`-Row, Git-Fehler im Log.
|
|
||||||
- **Feature-Walkthroughs:** Planning-Session-Flow (Draft→Finalize→Chain), Prime/Daily-Prep-Trigger, Weekly-Report-Generierung, Self-Update (Banner → Update → „up to date").
|
|
||||||
- **UI-Sichtprüfung (neu, 2026-06-09):** Diff-Viewer (Dateiliste, Added/Deleted/Renamed/Binary-Erkennung, Commit-Range-Diff nach Merge) und das „children need attention"-Band auf dem Session-Tab des Parents.
|
|
||||||
- **UI-Sichtprüfung (neu, 2026-06-10, nach Refactoring-Merges):** Detail-Insel komplett durchklicken (Output/Git/Session-Tabs, Merge-Sektion, Agent-Settings-Overrides, Prep-Panel) — `DetailsIslandViewModel` wurde in Sektions-VMs aufgeteilt, Bindings angepasst. Außerdem: DiffModal-Fehler-State „Diff nicht mehr verfügbar" (Commit-Range ohne aufgezeichnete Commits) und der In-App-Konflikt-Resolver (Hub-Methoden umbenannt).
|
|
||||||
- **UI-Sichtprüfung (neu, 2026-06-19, Rider-Style 3-Pane Merge-Editor):** Echten Konflikt auslösen (Single-Task-Approve mit Konflikt **und** Planning-Unit-Merge) und prüfen: drei Panes (Ours read-only | Result editierbar | Theirs read-only), Konfliktblöcke rot / aufgelöst grün in allen Panes, Inline-Accept `›`/`‹` in den Zwischen-Guttern landen die jeweilige Seite im Result, nur Konfliktregionen im Result editierbar (Stable read-only), synchrones vertikales Scrollen, File-Switcher bei mehreren Dateien, `M conflicts · K resolved`-Readout, Continue erst bei allen Konflikten gelöst, Binär-Guard. **Bekannte Kanten:** (1) Konflikt mit leerer Ours-Seite → Result-Region ist null-lang (Gutter via 1-Zeichen-Probe positioniert, Accept funktioniert; nur Hand-Tippen in die leere Region ist fummelig). (2) Gutter-Y nutzt `TranslatePoint` vom Result-`TextView` — bei sehr hohen Fenstern / großen Scrollständen die Ausrichtung gegenprüfen. (3) Blöcke richten sich nur über Stable-Text aus; nach einem Konflikt mit unterschiedlicher Zeilenzahl je Seite driften nachfolgende Blöcke vertikal (aligned/virtual-space Scroll ist bewusst zurückgestellt).
|
|
||||||
- **Worker-Autostart am Gerät:** Logoff/Logon-Autostart, Update-Pfad, Uninstall entfernt die Startup-`.lnk`.
|
|
||||||
- **In-App Interactive Sessions (neu, 2026-06-26):** ersetzt den externen `wt`-„Run interactively"-Launch durch einen In-App-Streaming-Chat (`StreamingClaudeSession`, `claude --input-format stream-json`). Real-CLI-Smoke (kein xUnit, kein Claude in Tests):
|
|
||||||
- Task rechtsklick → „Run interactively" startet **keinen** Terminal mehr; der Stream erscheint im Detail-Output-Tab des (selektierten) Tasks und als Monitor in Mission Control.
|
|
||||||
- Composer: Nachricht tippen + Enter/Send → erscheint sofort als `log-user`-Zeile in **Akzentfarbe** (via `LogKindForegroundConverter`, lokale Bindung schlägt den dim Style), Claude antwortet im selben Prozess.
|
|
||||||
- **Senden während Claude arbeitet = Queue (Default):** die Nachricht wird gepuffert und beim `result` des laufenden Turns abgeschickt (kein Interrupt). Mehrere Queue-Nachrichten FIFO, eine pro Turn. Gequeute Nachrichten erscheinen in einem **Pending-Streifen über der Eingabezeile** (⧗-Liste, via `InteractiveQueueChanged`); eine Nachricht landet erst im Transkript (`log-user`-Zeile via `InteractiveMessageSent`), wenn sie tatsächlich an Claude zugestellt wird. Der seeded Erst-Prompt erscheint als erste User-Zeile. Jede gequeute Zeile hat ein **✕ zum Entfernen** (`RemoveQueuedInteractiveMessage`, by-text first-match; Worker re-broadcastet die Queue).
|
|
||||||
- **Interrupt opt-in:** der kleine ■-Stop-Button neben Send unterbricht den laufenden Turn (`control_request`/`interrupt`, verifiziert mit CLI 2.1.191; Abbruch-`result` = `error_during_execution`, als Turn-Ende behandelt) — danach flusht die ggf. gequeute Nachricht im selben Prozess mit erhaltenem Kontext. Stop-Button ist immer sichtbar solange live (Interrupt im Idle ist ein No-op; Turn-in-flight wird nicht in die UI gebroadcastet).
|
|
||||||
- Session-Ende: Prozess-Exit/Stop → `InteractiveSessionEnded`, Composer verschwindet, Monitor wird „done".
|
|
||||||
- **Sicht-Konsistenz:** Mission-Control-Composer (SessionTerminalView-Bottom-Row mit Send-Button) vs. Detail-Composer (WorkConsole-Shell-Prompt `❯ … [Send]`) sehen unterschiedlich aus — ggf. angleichen.
|
|
||||||
- **Drag-and-drop file attachments on the detail pane:** verify the "Drop to attach" hover overlay, drop round-trip (file appears in the list), "Add file…" picker, remove button, and that files land under `~/.todo-app/attachments/<taskId>/`. Also verify the MCP `AddTaskAttachment`/`ListTaskAttachments`/`RemoveTaskAttachment` tools and that a Running task refuses add/remove. (Manual; can't be unit-tested.)
|
|
||||||
|
|
||||||
## Offene Code-Punkte
|
- **„Resume planning session" ist grundsätzlich kaputt (Session-Id wird nie erfasst) + Fehler wird verschluckt:** Im UnfinishedPlanning-Modal macht **Resume** nichts. Ursache: `PlanningSessionManager.ResumeAsync` (PlanningSessionManager.cs:238) bricht mit `InvalidOperationException("No Claude session ID captured yet; cannot resume.")` ab, wenn `task.PlanningSessionId` leer ist — und der Setter `TaskRepository.UpdatePlanningSessionIdAsync` (TaskRepository.cs:322) hat **keinen einzigen Aufrufer** im Worker, d.h. `planning_session_id` wird nie befüllt (verifiziert an `verif §3 resume-discard`: `planning_session_id=NULL`, Token gesetzt, Drafts angelegt). Resume kann also **nie** erfolgreich sein. Zusätzlich verschluckt `TasksIslandViewModel.ResumePlanningSessionAsync` den ganzen Block in einem leeren `catch { }` (~Zeile 870) → der Button ist ein **stummer No-op**, kein Footer-Fehler, kein Dialog (verstößt gegen `feedback_ui_error_surfacing`). Hintergrund: die wt-Planning-Session ist interaktiv; ClaudeDo erfasst die claude-Session-Id dort nicht (analog zur bewusst nicht persistierten ConPTY-Session-Id, §5). Fix-Optionen: (a) beim Planning-Start/-Lauf die claude-Session-Id erfassen und via `UpdatePlanningSessionIdAsync` persistieren, damit Resume echt resumen kann; ODER (b) Resume entfernen/deaktivieren, wenn keine Session-Id vorliegt; in JEDEM Fall den Fehler statt des leeren `catch` surfacen. Verifiziert §3. §3.
|
||||||
|
- **Attachments: erster Drag&Drop der Session schlug einmalig fehl („An error occurred"):** Beim allerersten Drop-to-attach einer UI-Session zeigte die `DropStatus`-Zeile inline einen generischen Fehler und es wurde nichts persistiert (kein File, keine DB-Row); alle folgenden Drops derselben Session + der „Add file…"-Picker + Remove funktionierten fehlerfrei. Nicht reproduzierbar nach dem ersten Mal (App-Neustart nötig, um die „erster-Drop"-Bedingung wiederherzustellen). Kandidaten: transiente SQLite-Contention (der UI-Prozess schreibt `todo.db` direkt via `new TaskAttachmentRepository`, während der Worker dieselbe DB hält) ODER ein Fehler im Drop-Stream-Pfad (`IStorageFile.OpenReadAsync` im Code-Behind, außerhalb des `try` in `AddFilesAsync`). Falls es erneut auftritt: `AddFilesAsync`/`OnDrop` mit robusterem Error-Logging versehen (die genaue Exception-Message fehlt, weil `DropStatus` nur `{fileName}: {ex.Message}` zeigt). Verifiziert §9 (einmalig beobachtet). §9.
|
||||||
|
- **Agent-Settings-Gear weicht vom übrigen Settings-Icon ab:** Der Agent-Settings-Flyout-Button (`TaskHeaderBar.axaml:68`) rendert ein Unicode-Glyph `⚙` (`<TextBlock Text="⚙">`), während die Listen-Nav (`ListsIslandView.axaml:56`) und die Listen-Settings (`TasksIslandView.axaml:47`) das gefüllte `Icon.Settings`-PathIcon (Gear-StreamGeometry, IslandStyles.axaml:110) nutzen → optisch ein anderes Zahnrad. Angleichen: den `⚙`-TextBlock durch `<PathIcon Data="{StaticResource Icon.Settings}" .../>` ersetzen. Verifiziert §8 (User-Sichtprüfung). §8.
|
||||||
|
- **Session-Skills-Tab hat keinen Empty-State:** Bei 0 installierten Skills zeigt der Skills-Tab (Settings) nur eine nackte leere Fläche unter der Install-Zeile — kein erklärender Hinweis (z.B. „Noch keine Skills installiert — GitHub-URL oben einfügen"). Verifiziert §8 (frischer Zustand vor ponytail-Install). Kleiner Empty-State-Text ergänzen.
|
||||||
|
|
||||||
- **Status-Bar Live-Update:** Prüfen, ob `RunNow`-Enable/Disable pro Task-Row bei Connection-Change sauber re-evaluiert. Connection-Status lebt in `IslandsShellViewModel` / `WorkerConnectionModalViewModel` (es gibt keinen `StatusBarViewModel` mehr). Erst messen, dann ggf. fixen. Klein.
|
- **Diff-Viewer: reiner Rename schwach dargestellt:** Rename korrekt erkannt (`UnifiedDiffParser` → `DiffFileStatus.Renamed`, Badge `StatusCode="R"`), aber der File-Tree zeigt nur den neuen Namen + „+0 −0" ohne „alt → neu"-Pfad; liest sich wie „keine Änderung". Optional: alten Pfad + „renamed"-Label anzeigen. §1.
|
||||||
- **`AgentMcpTools` liegt in `LifecycleMcpTools.cs`** — beim Suchen irreführend; in eigene Datei verschieben. Ein-Minuten-Fix, lohnt keinen Agent-Lauf — beim nächsten Worker-Touch mitnehmen.
|
- **Header-TurnsText zeigt `0/max` für abgeschlossene Runs:** `TurnsText => {Turns}/{EffectiveMaxTurns}` — `Turns` wird beim Laden eines terminalen Tasks nicht aus `task_runs.turnCount` restauriert (nur live gefüllt). Kosmetisch. §1.
|
||||||
|
- **Conflict-Resolver: Continue/Merge-Button klickbar trotz offener Konflikte:** Gate greift funktional (merged erst wenn alle Konflikte in allen Dateien gelöst), aber der Button ist nicht disabled/gegraut → früher Klick ist ein stummer No-op. Besser: disabled bis `AllResolved`, oder Hinweis „N Konflikte in M Dateien offen". §4.
|
||||||
|
- **Conflict-Resolver: mehrere Konfliktdateien schlecht erkennbar:** Beim 2-Datei-Konflikt schwer zu sehen, dass zwei Dateien betroffen sind (File-Switcher/Anzahl zu unauffällig). Prominentere Datei-Liste / „x von y Dateien". §4.
|
||||||
|
- **Planning-aktiver Parent zeigt weiter „Idle":** Parent in `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber der Row-Status-Chip zeigt „Idle"; der `PlanningBadge` ersetzt das nicht sichtbar → liest sich wie ein normaler Idle-Task. Wunsch: klarer „Planning/Draft aktiv"-Zustand, der Idle überschreibt. (Kinder zeigen korrekt „Draft".) §3.
|
||||||
|
- **Blocked-by-Kette nicht sichtbar:** Nach Finalize ist die sequentielle Kette korrekt gesetzt (child[i] blocked-by child[i-1]), aber die UI stellt die Reihenfolge/Abhängigkeit nicht dar. Wunsch: Kette visualisieren (z.B. „wartet auf <Vorgänger>"), nicht nur der „waiting"-Chip nach dem Queueen. §3.
|
||||||
|
- **Dequeue-„X" fehlt auf wartenden (blockierten) Kettengliedern:** `CanRemoveFromQueue = IsQueued || HasQueuedSubtasks`, `IsQueued` verlangt leeres `blocked_by`. Ein gequeuetes, aber blockiertes Kind (`IsWaiting`) bekommt daher kein Remove-from-queue-X — nur Parent + erstes (entsperrtes) Kind. §3.
|
||||||
|
- **„Open ConPTY session" erneut = Prompt wird neu gesendet, kein Resume:** Da die ConPTY-Session-Id bewusst nicht persistiert wird, startet ein erneutes „Open ConPTY session" auf demselben Task eine frische Session und sendet den Task-Prompt erneut (re-runt die Arbeit im selben Worktree) statt zu resumen. So designt, aber UX-Falle — evtl. „Resume"-Affordance oder Re-Open-Warnung. §5.
|
||||||
|
|
||||||
## Nachklapp Refactoring-/Bug-Runde (2026-06-09/10)
|
## Feature-Wünsche (aus der Session)
|
||||||
|
|
||||||
Alle 9 Review-Tasks (5 Refactorings, 4 Bugfixes) sind umgesetzt und gemerged; Details in den Commits. Offen geblieben:
|
- **Conflict-Resolver: farbliches Hervorheben eingefügter Zeilen im Result-Pane** (grüner „flow" der übernommenen Zeilen). §4.
|
||||||
|
- Als ClaudeDo-Tasks in Liste „Claude do" erfasst: **Approve erzwingt Diff/Review vor Merge** (koppelt den blocked-Merge-Silent-Fail-Fix) und **interaktive Planning-Session über embedded ConPTY statt externem wt-Fenster** (koppelt den Planning-Permission-Prompt).
|
||||||
|
|
||||||
- **`DetailsIslandViewModel` ist nach dem Split noch 1258 Zeilen** (Ziel war ~800) — die drei Sektions-VMs (AgentSettings, Merge, Prep) sind extrahiert, weitere Extraktion (z.B. ChildOutcomes/Subtasks-Sektion) lohnt erst, wenn die Datei wieder wächst.
|
## Sichtprüfung offen (2026-07-27, Batch „Claude do"-Liste)
|
||||||
- **Bewusst zurückgestellt:** WorkerHub-Split nach Concern (~60 Methoden in einer Hub-Klasse). Die Interface-Parität löst das akute Testbarkeits-Problem; ein Hub-Split ist eine größere Architekturentscheidung → erst besprechen.
|
|
||||||
- **Lessons learned:** Der `StartRunningAsync`-Guard-Task hat isoliert grün getestet, aber den Queue-Pfad gebrochen (Picker claimt vor dem Dispatch) — Integrationsfix `74ca2e0`. Bei parallelen Tasks, die denselben Pfad berühren, nach JEDEM Merge-Schwung die volle Suite auf main fahren.
|
|
||||||
|
|
||||||
## Bug-Befunde (Korrektheits-Review 2026-06-09)
|
Alles gebaut + unit-getestet, aber **nicht visuell verifiziert** — Mika prüft:
|
||||||
|
|
||||||
**Plausibel, noch nicht einzeln verifiziert (bei Gelegenheit prüfen):**
|
- **Ctrl+K fokussiert die Suche, `#` ist wieder tippbar** (Binding war `OemQuestion` = `#` auf DE-Layout).
|
||||||
|
- **Titel-Edit in der Detail-Insel persistiert** (400 ms debounced, wie die Beschreibung); Row-Titel + Merge-Kontext ziehen mit.
|
||||||
|
- **Spinner beim ConPTY-Start:** Tile erscheint sofort mit „Sitzung wird gestartet…" und deckt Launch-Spec-Roundtrip + Spawn ab. **Nicht** abgedeckt: die Sekunden, die die claude-TUI danach zum ersten Frame braucht (dafür bräuchte es einen Hook auf `TerminalControl.DataReceived`). Bei Launch-Fehler bleibt das Tile jetzt mit Inline-Banner stehen statt zu verschwinden.
|
||||||
|
- **Spinner beim Refine** ersetzt den Refine-Button in der Row, solange der Run läuft.
|
||||||
|
- **Interactive-Chip** statt „Parked" auf Tasks mit offener ConPTY-Session; Klick öffnet Mission Control und fokussiert die Pane. Accent-Tint (bewusst dieselbe „live"-Familie wie Running, klar unterschieden vom slate-blauen Parked).
|
||||||
|
- **Diff-Viewer:** rechter Abstand der `+n −n`-Zahlen im File-Tree.
|
||||||
|
- **Manual-Tasks:** MANUAL-Badge, Kontextmenü-Toggle, Listen-Checkbox „Manuelle Liste"; Queue/Refine/Planning ausgeblendet, ConPTY bleibt.
|
||||||
|
- **Settings → Allgemein:** Tabelle „Vorgaben pro Modell" (Effort + Max. Durchläufe je haiku/sonnet/opus/fable). Ersetzt das einzelne globale „Max. Durchläufe"-Feld.
|
||||||
|
|
||||||
- Cancel eines `WaitingForChildren`-Parents kaskadiert nicht auf laufende/queued Kinder (verwaiste Worktree-Commits).
|
Offene Entscheidungen dazu:
|
||||||
- Ketten-Kaskade stoppt an einem `Idle`-Mittelglied (`OnChildFinishedAsync` prüft `CancelAsync`-Ergebnis nicht) → Rest bleibt `Queued+blocked`.
|
|
||||||
- Delete des *letzten* nicht-terminalen Kindes triggert kein `TryAdvanceParentAsync` → Parent kann in `WaitingForChildren` hängen (FK `SET NULL` rettet nur die Blocked-Kette).
|
|
||||||
- `ContinueMergeAsync` staged per `git add -A` vor dem Konflikt-Check (Marker im Index, Abort danach ggf. unsauber).
|
|
||||||
- `HasChangesAsync` zählt untracked Files → blockiert Merges unnötig (`--untracked-files=no`).
|
|
||||||
- `UnifiedDiffParser`: Pfade mit Leerzeichen / git-gequotete Pfade aus `diff --git` falsch geparst.
|
|
||||||
- Kleinkram: MergePreview-Race bei schnellem Target-Wechsel, CTS-Dispose-Leak in Debounce-Saves, `Environment.CurrentDirectory`-Fallback im Konflikt-Dialog, Doppel-Continue-Fenster im Orchestrator.
|
|
||||||
|
|
||||||
**Geprüft und verworfen (keine Bugs):** ReviewFeedback-„Endlosschleife" (Fallback existiert), Cross-Thread-Crashes im DetailsIslandViewModel (Dispatcher-Marshalling im WorkerClient), Chain-Wedge nach Child-Delete (FK `ON DELETE SET NULL`), `\ No newline`-Parsing.
|
- Interaktive ConPTY-Sessions bekommen `--effort`, aber **kein** `--model` — die Session läuft weiter unter dem Modell aus Mikas Claude-Config, der Effort kommt aus dem Preset des Modells, das ClaudeDo für die Task auflösen würde. Falls ClaudeDo auch interaktiv das Modell erzwingen soll, ist das ein Folge-Task.
|
||||||
|
- `AppSettings.DefaultMaxTurns` ist jetzt tatsächlich verdrahtet (`ModelPresets.For(..., global.DefaultMaxTurns)` in `TaskRunner.ResolveConfigAsync`): reiner Fallback für ein Modell, das auch nach `ModelRegistry.TryNormalizeAlias` auf keine Preset-Zeile trifft — vorher war das Feld tot (hartkodierte 30). Hat weiterhin keinen eigenen Editor mehr (nur die Preset-Tabelle pro Modell); Spalte könnte später entfallen, falls das nie zutrifft.
|
||||||
|
|
||||||
|
## Beobachtung (offen — Entscheidung Mika)
|
||||||
|
|
||||||
|
- **`--permission-mode auto` + Modell `haiku` → Writes werden denied:** Kontrolliert verifiziert (CLI 2.1.207): unter dem Default-Mode `auto` bekommt **sonnet** Writes auto-approved (`permission_denials:[]`), **haiku** wird `denied` (`permission_denials:[Write]`, keine Datei) — eine haiku-Task macht unter `auto` still nichts und landet ohne Änderung in `WaitingForReview`. Normalbetrieb (Default = sonnet) nicht betroffen. KEINE CLI-Regression, sondern modellabhängiges `auto`-Verhalten. Optionen falls es nervt: haiku aus der Auswahl nehmen, ODER Runner auf `acceptEdits`/`bypassPermissions` (modell-unabhängig). Mika: erstmal beobachten. Siehe Memory `auto_permission_haiku_footgun`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Verifikation (2026-07-27)
|
||||||
|
|
||||||
|
- **List handler (2026-07-27)** — visual pass: the Broom button is gone from the lists footer,
|
||||||
|
the context-menu item appears only on lists with a working dir, and the selection dialog has no
|
||||||
|
LIST column. Plus a real-Claude smoke run of the five phases (dedupe questions, enhancements
|
||||||
|
landing in task descriptions, queued execution, merges).
|
||||||
|
|
||||||
|
## Offene Verifikation (2026-08-05)
|
||||||
|
|
||||||
|
- **List handler owns a task (2026-08-05)** — build + unit tests all green, but **not visually
|
||||||
|
verified**: start "Let Claude handle it" on a list, confirm exactly one new task appears in that
|
||||||
|
list (`Idle`, MANUAL badge, title "List handler: <list>"), the Mission Control tile is
|
||||||
|
task-based (Submit for review button present), Submit for review flips it to
|
||||||
|
`WaitingForReview`, and the detail pane's diff/merge card shows the full range of everything the
|
||||||
|
run merged to main (via the new `HandlerBaseCommit`/`HandlerHeadCommit` fallback — no
|
||||||
|
`WorktreeEntity` is created for this task, so the diff comes from `list.WorkingDir` directly).
|
||||||
|
- **Roadblock reply field (2026-08-05)** — build + unit tests all green, but **not visually
|
||||||
|
verified**: on a `Done` (or `WaitingForReview`/`Failed`/`Cancelled`) task with a reported
|
||||||
|
roadblock, the ROADBLOCK card shows a reply textbox + Send button under the roadblock text.
|
||||||
|
Check layout/spacing against the AskUser question card it's modeled on, Enter-to-send, the
|
||||||
|
disabled state + hint text when there's no session ID to resume, and that an override-slot-busy
|
||||||
|
error shows up in the footer log strip (not a modal) with the typed text still in the field.
|
||||||
|
Approve should go straight to `Done` with no merge attempt. Design choice: the review range
|
||||||
|
lives as two new nullable columns directly on `TaskEntity` (not a phantom `WorktreeEntity` row),
|
||||||
|
specifically so `list_worktrees`/the Worktrees overview never see it.
|
||||||
|
|
||||||
|
- **Post-merge verify gate (2026-08-05)** — build + unit tests all green (incl. real-process
|
||||||
|
`VerifyCommandRunner` exit-code/output/timeout tests and `TaskMergeService` success/failure/
|
||||||
|
timeout paths via a fake runner), but **not visually verified**: open a list's Settings modal,
|
||||||
|
confirm the new "VERIFICATION" section renders below Agent with a settable/clearable
|
||||||
|
`VerifyCommand` field; approve a task on a list with a failing command configured and confirm
|
||||||
|
the footer/error surfacing (`ShowErrorAsync`) actually shows the verify failure message instead
|
||||||
|
of silently looking like nothing happened. Also no real-build smoke test (a real `dotnet build`/
|
||||||
|
`dotnet test` invocation as the configured command) — only fast synthetic commands (`exit N`,
|
||||||
|
`ping` for timeout) were exercised.
|
||||||
|
|
||||||
|
## Offene Verifikation (2026-08-05, Usage Monitor)
|
||||||
|
|
||||||
|
- **Visueller Pass Usage-Pill** (Footer **und** Mission-Control-Header): Text/Tooltip,
|
||||||
|
Dot-Zustände normal/warn/stale/blocked, Dark/Light.
|
||||||
|
- **Visueller Pass Usage-Monitor-Modal**: Gauges (dynamisch aus `limits[]`), Modell-/Task-
|
||||||
|
Tabellen, Info-Bänder (stale/gate-blocked), 7d/30d-Presets + Custom-Range, Dark/Light.
|
||||||
|
- **E2E Gate**: das Gate greift real, sobald ein Bucket (`five_hour`/`seven_day`) die
|
||||||
|
konfigurierte Schwelle reißt — Queue-Nachschub pausiert, laufende Runs/`RunNow`/ConPTY/
|
||||||
|
Planning/Prime bleiben unberührt — und die Queue nimmt nach dem Reset selbstständig wieder
|
||||||
|
auf (30s-Backstop, kein persistenter Pause-Zustand).
|
||||||
|
- **Risiko:** der Usage-Endpoint (`GET https://api.anthropic.com/api/oauth/usage`) ist
|
||||||
|
undokumentiert und kann sich ändern; bei Ausfall/Formatänderung ist das Gate wirkungslos
|
||||||
|
(fail-open by design — kein Blocker, aber der Schutz fällt dann aus, ohne dass es auffällt).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bewusst verworfen (nicht erneut vorschlagen)
|
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||||
|
|
||||||
- **CI-Build/Test-Pipeline** — push-to-main + release-on-push deckt das ab; Tests laufen am Ende jeder Session.
|
- **CI-Build/Test-Pipeline** — push-to-main + release-on-push deckt das ab; Tests laufen am Ende jeder Session.
|
||||||
- **Real-`claude`-Smoke-Test als xUnit-Test** — kein Claude in `dotnet test`; bleibt manueller Check (siehe oben). Tests nutzen `FakeClaudeProcess`.
|
- **Real-`claude`-Smoke-Test als xUnit-Test** — kein Claude in `dotnet test`; bleibt manueller Check. Tests nutzen `FakeClaudeProcess`.
|
||||||
- **`architecture.md` / ADRs** — die per-Projekt-`CLAUDE.md`-Dateien sind die lebende Doku; ADRs lohnen solo nicht.
|
- **`architecture.md` / ADRs** — die per-Projekt-`CLAUDE.md`-Dateien sind die lebende Doku.
|
||||||
- **Task-Mailbox-Integration** — geparkt; das generische `mcp__mailbox__*`-Plugin reicht (Begründung in `mailbox-proposal.md`).
|
- **Task-Mailbox-Integration** — geparkt; das generische `mcp__mailbox__*`-Plugin reicht (`mailbox-proposal.md`).
|
||||||
- **Tag-Negation, Tag-Multi-Select, Notes-`lists.kind`-Switch, Install-Service-Skript** — durch die aktuelle Architektur überholt (Tag-System entfernt, Notes/Autostart anders gelöst).
|
- **Tag-Negation, Tag-Multi-Select, Notes-`lists.kind`-Switch, Install-Service-Skript** — durch die aktuelle Architektur überholt.
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Session Skills — Implementation Plan
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-07-03-session-skills-design.md`
|
||||||
|
Approach: subagent-driven TDD (sonnet), build + test + commit per task, stage files by
|
||||||
|
path (never `git add -A`).
|
||||||
|
|
||||||
|
**Pre-flight (do first, before building anything):** manual smoke test — drop a skill
|
||||||
|
into a scratch worktree's `.claude/skills/` and run `claude -p` to confirm cwd skills are
|
||||||
|
discovered in headless mode. The whole feature rests on this. If it fails, stop and
|
||||||
|
redesign around `CLAUDE_CONFIG_DIR`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 — Data layer: columns + registry table + migration
|
||||||
|
|
||||||
|
- Add nullable `SessionSkills` (string, JSON array) to `TaskEntity`, `ListConfigEntity`,
|
||||||
|
`AppSettingsEntity`; map `session_skills` columns in their `*Configuration.cs`.
|
||||||
|
- New `SessionSkillEntity` (`name` PK, `source_url`, `pinned_ref`, `subpath`,
|
||||||
|
`description`, `added_at`) — one row per skill; a multi-skill repo writes N rows sharing
|
||||||
|
`source_url`/`pinned_ref` — + configuration + `session_skills` table.
|
||||||
|
- New `SessionSkillRepository` (async, CancellationToken): `ListAsync`, `GetAsync(name)`,
|
||||||
|
`UpsertAsync`, `DeleteAsync(name)`, `DeleteBySourceAsync(url)`, `ListBySourceAsync(url)`.
|
||||||
|
- EF migration `AddSessionSkills` (columns + table).
|
||||||
|
- **Tests (Data.Tests):** repository CRUD on real SQLite; JSON column round-trips a
|
||||||
|
name list.
|
||||||
|
|
||||||
|
## Task 2 — Registry service (install / update / remove)
|
||||||
|
|
||||||
|
- `Skills/SessionSkillRegistry` + `Skills/Interfaces/ISessionSkillRegistry`,
|
||||||
|
`IRepoCloner` (clone abstraction so tests inject a local source dir).
|
||||||
|
- `GitRepoCloner` (production) does `git clone` + resolves HEAD SHA.
|
||||||
|
- Install: clone → **detect layout** (`skills/*/SKILL.md` bundle → each subskill; else
|
||||||
|
root `SKILL.md` → single; else reject) → per skill parse YAML frontmatter (`name`,
|
||||||
|
`description`), copy its dir **flat** to `~/.todo-app/session-skills/<name>/`, upsert a
|
||||||
|
row with `subpath`. Reject collision with a skill from a different source; reinstalling
|
||||||
|
the same source refreshes.
|
||||||
|
- Update(sourceUrl) / Remove(sourceUrl) per spec (act on all of a source's skills).
|
||||||
|
- **Tests (Worker.Tests):** install a **multi-skill** fixture (fake cloner, mirrors
|
||||||
|
ponytail's `skills/*/SKILL.md`) → N rows + N flat dirs; install a root-`SKILL.md`
|
||||||
|
fixture → 1 row; neither → rejected; cross-source name collision rejected;
|
||||||
|
remove-by-source deletes all its dirs + rows. **No real network / no real claude CLI.**
|
||||||
|
|
||||||
|
## Task 3 — Resolution: union into ClaudeRunConfig
|
||||||
|
|
||||||
|
- Add `IReadOnlyList<string> SkillNames` to `ClaudeRunConfig` (default empty).
|
||||||
|
- In `TaskRunner.ResolveConfigAsync`: parse each level's `session_skills`, union + dedup,
|
||||||
|
filter to registry-existing names (drop + log missing).
|
||||||
|
- **Tests (Worker.Tests):** union across the three levels; dedup; unknown name dropped.
|
||||||
|
|
||||||
|
## Task 4 — Seeder
|
||||||
|
|
||||||
|
- `Skills/SessionSkillSeeder` + interface. `SeedAsync(cwd, skillNames, isWorktree, ct)`:
|
||||||
|
copy each installed skill dir → `<cwd>/.claude/skills/<name>/`; if worktree, append
|
||||||
|
`/.claude/skills/<name>/` to `git rev-parse --git-path info/exclude` target if absent.
|
||||||
|
- Wire into `TaskRunner` after run-dir resolution, before `ClaudeProcess.RunAsync`
|
||||||
|
(both worktree and sandbox paths).
|
||||||
|
- **Tests (Worker.Tests):** seeds into real temp dir; idempotent re-seed; worktree
|
||||||
|
exclude line written once and not duplicated; seeded path is git-ignored (real git
|
||||||
|
temp repo → `git status` clean for the seeded dir).
|
||||||
|
|
||||||
|
## Task 5 — Hub + DTOs + client
|
||||||
|
|
||||||
|
- `WorkerHub`: `GetSessionSkills`, `InstallSessionSkill(url)`, `UpdateSessionSkill(name)`,
|
||||||
|
`RemoveSessionSkill(name)`.
|
||||||
|
- New `SessionSkillDto`; extend `AppSettingsDto`, `ListConfigDto`, `UpdateListConfigDto`,
|
||||||
|
`UpdateTaskAgentSettingsDto` with skill-name lists; map in the update handlers.
|
||||||
|
- `IWorkerClient` + `WorkerClient` additions.
|
||||||
|
- **Update hand-rolled fakes** in Worker.Tests + Ui.Tests (memory
|
||||||
|
`iworkerclient_fakes_sync`).
|
||||||
|
- **Tests:** hub method round-trip via existing hub test harness where present.
|
||||||
|
|
||||||
|
## Task 6 — UI: registry tab + selectors
|
||||||
|
|
||||||
|
- `SessionSkillsSettingsTabViewModel` + a **Skills** tab in `SettingsModalView.axaml`:
|
||||||
|
installed list, Add (URL), Update, Remove, status line. Mirror
|
||||||
|
`FilesSettingsTabViewModel`.
|
||||||
|
- Global multi-select in General settings tab → `AppSettings.SessionSkills`.
|
||||||
|
- Skills multi-select in shared `AgentConfigEditor` (covers List + Task) with inheritance
|
||||||
|
badge, wired through `AgentConfigEditorViewModel`.
|
||||||
|
- Localization: add EN + DE keys in parity (Localization.Tests enforces).
|
||||||
|
- **Tests (Ui.Tests / Localization.Tests):** VM load/save of selections; locale parity.
|
||||||
|
- **Visual verification is Mika's** — flag the gaps.
|
||||||
|
|
||||||
|
## Task 7 — Wiring, build, end-to-end smoke
|
||||||
|
|
||||||
|
- DI registration (registry, cloner, seeder) in `Program.cs`.
|
||||||
|
- Build all touched projects `-c Release`; run Worker/Data/Ui/Localization test projects.
|
||||||
|
- Manual E2E: install ponytail via the UI, enable per-task, run a task, confirm the skill
|
||||||
|
is available to the agent and **not** committed and **not** in interactive sessions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Commit per task with Conventional Commits (`feat(worker|ui|data): …`). Commit the
|
||||||
|
spec + plan docs first.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Plan — ConPTY Interactive Sessions
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-07-23-conpty-interactive-sessions-design.md`
|
||||||
|
Date: 2026-07-23
|
||||||
|
|
||||||
|
Execution: subagent-driven-development, sonnet model, TDD where meaningful,
|
||||||
|
build + test + commit per task. Stage files explicitly by path (never
|
||||||
|
`git add -A`). Terminal rendering is visual — flagged for the user's visual pass.
|
||||||
|
|
||||||
|
## Task 0 — Spike: embed a ConPTY terminal running `claude`
|
||||||
|
|
||||||
|
Not TDD; a throwaway proof. Add a temporary window/view that embeds each
|
||||||
|
candidate control and launches `claude` in a known worktree.
|
||||||
|
|
||||||
|
- Evaluate **SvcSystems.UI.Terminal** and **Iciclecreek.Avalonia.Terminal**.
|
||||||
|
- Acceptance: the real `claude` TUI renders correctly — colors, resize/reflow,
|
||||||
|
and a live permission prompt is usable; input reaches the CLI.
|
||||||
|
- Output: pick one library; note the control API (start with cwd/exe/args/env,
|
||||||
|
process-exited event, dispose/kill). Record the decision in the spec.
|
||||||
|
- Remove the throwaway harness before Task 1 (or keep as a manual dev sample,
|
||||||
|
not wired into the app).
|
||||||
|
|
||||||
|
**Stop for the user's visual verification of the spike before continuing.**
|
||||||
|
|
||||||
|
## Task 1 — Worker: interactive launch-spec endpoint
|
||||||
|
|
||||||
|
- Add a Worker service/hub method that, given a taskId, prepares the worktree
|
||||||
|
(session-skills seeding, agent files, MCP config, env — reuse the autonomous
|
||||||
|
run prep path) and returns a `LaunchSpec { cwd, exe, args, env }`.
|
||||||
|
- Reuse `WindowsTerminalLauncher.BuildResumeCommand` for exe/args.
|
||||||
|
- Guards mirror `ResumeTaskInTerminal` (not Running/Queued, persisted SessionId,
|
||||||
|
worktree Active/Kept). Never-run task → spec without `--resume` (fresh start).
|
||||||
|
- Tests (Worker.Tests, real SQLite/git): guard cases, spec contents for a
|
||||||
|
resumable task, fresh-start case. No real `claude` in tests.
|
||||||
|
|
||||||
|
## Task 2 — Worker: ad-hoc launch-spec
|
||||||
|
|
||||||
|
- Method to build a `LaunchSpec` for a free session in a given directory:
|
||||||
|
MCP config + env set up, no task/session-skills seeding.
|
||||||
|
- Tests: env/MCP presence, arbitrary cwd.
|
||||||
|
|
||||||
|
## Task 3 — UI: terminal host control + view model
|
||||||
|
|
||||||
|
- Wrap the chosen library in an app control/view (e.g. `InteractiveTerminalView`
|
||||||
|
+ `InteractiveTerminalViewModel`) that starts from a `LaunchSpec` and exposes
|
||||||
|
running/exited state.
|
||||||
|
- `IWorkerClient`: add methods to fetch the task and ad-hoc launch specs; wire
|
||||||
|
the SignalR client + hub method.
|
||||||
|
- Update hand-rolled `IWorkerClient`/hub fakes in BOTH test projects.
|
||||||
|
- Tests: view model starts/stops lifecycle with a fake terminal backend;
|
||||||
|
fake worker returns a spec.
|
||||||
|
|
||||||
|
## Task 4 — Command Center: host interactive panes + entry points
|
||||||
|
|
||||||
|
- `MonitorPaneView`: autonomous panes keep the streamed log; interactive panes
|
||||||
|
host the terminal control.
|
||||||
|
- Entry points: "Open interactive session" from a task (task-based) and a
|
||||||
|
"New session" action (ad-hoc, pick directory).
|
||||||
|
- Layout toggle: focus (tabs) ↔ overview (grid); reuse/extend the existing
|
||||||
|
`UniformGrid` column logic for the grid mode.
|
||||||
|
- Tests: view-model level (pane kind selection, layout toggle state). Rendering
|
||||||
|
is a visual-pass item.
|
||||||
|
|
||||||
|
## Task 5 — Remove the streaming interactive stack
|
||||||
|
|
||||||
|
Only after Tasks 1–4 land and the terminal path works.
|
||||||
|
|
||||||
|
- Worker: delete `StreamingClaudeSession`, `InteractiveSessionService`,
|
||||||
|
interactive `WorkerHub` methods + broadcast events, DI registrations.
|
||||||
|
Verify `LiveSessionRegistry` / `IdleSessionReaper` usage first; remove only if
|
||||||
|
unreferenced.
|
||||||
|
- UI: remove composer bits on `TaskMonitorViewModel`, the composer/queued portion
|
||||||
|
of `SessionTerminalView`, `IWorkerClient` interactive methods.
|
||||||
|
- Update fakes and delete now-dead tests. Full build + all test projects green.
|
||||||
|
|
||||||
|
## Task 6 — Docs
|
||||||
|
|
||||||
|
- Update `docs/open.md` with visual-verification items (spike render, terminal
|
||||||
|
resize/focus, grid vs tabs).
|
||||||
|
- Update affected per-project `CLAUDE.md` (Worker interactive removal, UI new
|
||||||
|
terminal host).
|
||||||
|
|
||||||
|
## Verification gates
|
||||||
|
|
||||||
|
- After Task 0: user visual pass on the spike.
|
||||||
|
- After Task 4: user visual pass on Command Center (task + ad-hoc, tabs + grid,
|
||||||
|
permission prompt round-trip).
|
||||||
|
- Never claim the terminal UI works without the user running it.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Merge Helper — Implementation Plan
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-07-24-merge-helper-design.md`
|
||||||
|
Approach: subagent-driven (one subagent per task, `sonnet`, TDD, stage files by path — never `git add -A`). Build with `-c Release` per-csproj (a running Worker locks `Debug`). Commit per task, Conventional Commits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase A — Worker MCP conflict tools
|
||||||
|
|
||||||
|
Independently useful; merges first. All in `src/ClaudeDo.Worker/External/ExternalMcpService.cs` + tests in `tests/ClaudeDo.Worker.Tests/`.
|
||||||
|
|
||||||
|
### A1 — Verify engine surface (spike, no commit)
|
||||||
|
Read `TaskMergeService.MergeAsync` / `ContinueMergeAsync` / `AbortMergeAsync` and the hub conflict flow (`WorkerHub.StartConflictMerge`/`ContinueConflictMerge`/`AbortConflictMerge`). Pin down:
|
||||||
|
- exact `ContinueMergeAsync` / `AbortMergeAsync` signatures and how in-progress-merge state is located (repo + target branch from task/list, not shared hub state);
|
||||||
|
- how the childless approve path (`ApproveAndMergeAsync`) threads `leaveConflictsInTree`.
|
||||||
|
Record findings in the task notes; feeds A2/A3.
|
||||||
|
|
||||||
|
### A2 — `leaveConflictsInTree` on review_task / merge_task
|
||||||
|
- TDD: tests in `Worker.Tests` (real git) — clean merge → Done; conflict + flag → `conflict_in_tree`, markers present, task stays `WaitingForReview`, `repoPath` returned.
|
||||||
|
- Add optional param `leaveConflictsInTree = false` to `MergeTask` and `ReviewTask` (approve branch). When true, call the `leaveConflictsInTree:true` engine path and map the conflict result to `{ mergeStatus/merged, conflicts, repoPath }`.
|
||||||
|
- Keep default behaviour (abort-on-conflict) byte-identical when the flag is absent/false.
|
||||||
|
- Commit: `feat(worker): let review_task/merge_task leave conflicts in tree via MCP`
|
||||||
|
|
||||||
|
### A3 — `continue_merge` + `abort_merge` MCP tools
|
||||||
|
- TDD: continue after on-disk resolution → committed, task Done, worktree merged; continue with markers remaining → returns conflicts; abort → markers gone, task `WaitingForReview`; both on no-active-merge → clean MCP error; `TaskUpdated` fired.
|
||||||
|
- Add `[McpServerTool] continue_merge(taskId)` → `ContinueMergeAsync`; `abort_merge(taskId)` → `AbortMergeAsync`. Locate the merge from the task's repo/target. Emit `TaskUpdated`.
|
||||||
|
- **Route both single-task and orchestrated (parent/children) in-progress merges** where locatable from the task (per A1 findings): detect the kind and call the matching engine continue/abort (`TaskMergeService` vs `PlanningMergeOrchestrator.Continue/Abort`). If the orchestrated path can't be located without hub UI state, leave it to the manual fallback (documented in the B1 prompt) and note the gap in `docs/open.md`.
|
||||||
|
- Commit: `feat(worker): add continue_merge and abort_merge MCP tools`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B — Worker launch for the merge-helper session
|
||||||
|
|
||||||
|
### B1 — Prompt templates
|
||||||
|
- Add `PromptKind.MergeHelper` + `PromptKind.MergeHelperInitial` to `ClaudeDo.Data/PromptFiles.cs` (file names `merge-helper-system.md` / `merge-helper-initial.md`, built-in `DefaultFor`, `Render` tokens for the initial brief).
|
||||||
|
- System prompt encodes §7 behaviour (per-status algorithm, ask-on-uncertainty, summary format). Merge-state rule: **prefer MCP tools whenever they apply**; hand-merge (Edit + `git commit -- <paths>`) is an accepted fallback only for merges the MCP tools can't reach (§5.3), never a shortcut around them.
|
||||||
|
- Initial brief renders a task table `{id,title,status,list,repo}` + scope label.
|
||||||
|
- TDD: `PromptFiles` tests — kinds resolve, defaults non-empty, `Render` substitutes brief tokens.
|
||||||
|
- Commit: `feat(data): add merge-helper prompt templates`
|
||||||
|
|
||||||
|
### B2 — `BuildForMergeHelper` launch spec
|
||||||
|
- TDD (`Worker.Tests`): distinct-repo `--add-dir` set computed from selected tasks; correct cwd per scope (per-list repo vs first repo global); brief file written to `~/.todo-app/merge-helper-sessions/<guid>/brief.md`; allowed-tools + `--permission-mode default` + `MCP_TOOL_TIMEOUT` env correct; single-line kickoff points at the brief.
|
||||||
|
- Implement `InteractiveLaunchSpecService.BuildForMergeHelper(IReadOnlyList<string> taskIds, MergeHelperScope scope, ct)`. Reuse the planning brief-file/kickoff pattern.
|
||||||
|
- Commit: `feat(worker): build merge-helper interactive launch spec`
|
||||||
|
|
||||||
|
### B3 — Hub endpoint + client method
|
||||||
|
- `WorkerHub.GetMergeHelperLaunchSpec(string[] taskIds, string? listId)`; `IWorkerClient.GetMergeHelperLaunchSpecAsync(...)` + `WorkerClient` impl.
|
||||||
|
- Update hand-rolled `IWorkerClient` fakes in **both** test projects (see gotcha memory).
|
||||||
|
- Commit: `feat(worker): expose merge-helper launch spec over the hub`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase C — UI
|
||||||
|
|
||||||
|
### C1 — Selection dialog (View + VM)
|
||||||
|
- New `MergeHelperSelectionViewModel` + `MergeHelperSelectionDialog.axaml` (compiled bindings, `TaskCompletionSource<T>` pattern). Checkbox rows (title, status badge, list/repo), grouping in global mode, default ticks per §4, select-all/none, confirm disabled when empty.
|
||||||
|
- Candidates via existing `list_tasks`/worker client; filter client-side.
|
||||||
|
- TDD (`Ui.Tests`): default-tick logic, empty→confirm-disabled, returns ordered selected IDs + list mapping.
|
||||||
|
- Commit: `feat(ui): add merge-helper task selection dialog`
|
||||||
|
|
||||||
|
### C2 — Entry points + event plumbing
|
||||||
|
- Per-list context-menu item **"Let Claude handle it"** in `ListsIslandView.axaml` (user-list rows) + one global entry in the footer. Bind to `LetClaudeHandleCommand` on `ListsIslandViewModel` (param = `ListNavItemViewModel` or a global sentinel).
|
||||||
|
- VM raises `LetClaudeHandleRequested(MergeHelperScope)`; `IslandsShellViewModel` forwards to Mission Control.
|
||||||
|
- Commit: `feat(ui): add "Let Claude handle it" entry points`
|
||||||
|
|
||||||
|
### C3 — Mission Control wiring
|
||||||
|
- `MissionControlViewModel.OpenMergeHelperConPtySessionAsync(scope)`: open selection dialog → on confirm, `GetMergeHelperLaunchSpecAsync` → wrap in `TerminalLaunchDescriptor` → new `ConPtyPaneViewModel` (never deduped) → add to `ConPtySessions`/`Panes`.
|
||||||
|
- Commit: `feat(ui): open merge-helper ConPTY tile from selection`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verify (per task + at the end)
|
||||||
|
- Read each subagent diff; build the touched csproj `-c Release`; run the relevant test project.
|
||||||
|
- `locales/en.json` + `de.json` parity for any new UI strings (Localization.Tests enforces it).
|
||||||
|
- Flag visual-verification gaps (dialog layout, tile) for the user — never claim UI works without a run.
|
||||||
|
- End-to-end ConPTY smoke (real Claude) is a manual item in `docs/open.md`.
|
||||||
|
|
||||||
|
## Commit docs first
|
||||||
|
`docs(merge-helper): spec + implementation plan` (this file + the spec).
|
||||||
@@ -0,0 +1,938 @@
|
|||||||
|
# Per-List Task Handler 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 "Let Claude handle it" list-scoped only, and turn its prompt into a five-phase run — read all tasks, dedupe, enhance, queue, review+merge.
|
||||||
|
|
||||||
|
**Architecture:** Four independent commits. Two touch only leaf code (the MCP status tool, the prompt templates). One strips the global UI entry point. The last is an atomic sweep that makes `listId` non-nullable end to end and collapses the launch spec to a single repo — atomic because a half-flipped signature chain leaves nullable warnings scattered across a commit boundary.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, xUnit, Avalonia 12, EF Core + SQLite, CommunityToolkit.Mvvm.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-07-27-list-handler-design.md`
|
||||||
|
|
||||||
|
**Build note:** `dotnet build ClaudeDo.slnx` needs .NET 9 — build individual csproj with `-c Release` (a running Worker locks `Debug` output).
|
||||||
|
|
||||||
|
**Staging note:** the checkout is shared with parallel sessions. Always `git add -- <exact paths>` and `git commit -- <exact paths>`. Never `git add -A`, never a bare `git commit`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `update_task_status` accepts `Cancelled`
|
||||||
|
|
||||||
|
Dedupe needs to retire an **Idle** duplicate. Today nothing can: `UpdateTaskStatus` allows only
|
||||||
|
`Idle`/`Queued`, `cancel_task` only cancels a *running* task, and `review_task(decision="cancel")`
|
||||||
|
requires WaitingForReview/Running/Queued. `TaskStateService.CancelAsync` already owns the
|
||||||
|
transition and its side effects.
|
||||||
|
|
||||||
|
`BatchMcpTools.BatchUpdateTaskStatus` delegates to this same method, so batch cancel comes free.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs:264-300`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Append inside the `ExternalMcpServiceTests` class. `SeedTaskAsync` does not exist in this class —
|
||||||
|
seed inline the way the existing tests do, via `_lists` / `_tasks`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task<TaskEntity> SeedPlainTaskAsync(TaskStatus status)
|
||||||
|
{
|
||||||
|
var listId = Guid.NewGuid().ToString();
|
||||||
|
await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||||
|
var task = new TaskEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t",
|
||||||
|
Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore",
|
||||||
|
};
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTaskStatus_Cancelled_CancelsAnIdleTask()
|
||||||
|
{
|
||||||
|
var task = await SeedPlainTaskAsync(TaskStatus.Idle);
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTaskStatus(task.Id, "Cancelled", CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("Cancelled", dto.Status);
|
||||||
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.Equal(TaskStatus.Cancelled, loaded!.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTaskStatus_Done_StillRejected()
|
||||||
|
{
|
||||||
|
var task = await SeedPlainTaskAsync(TaskStatus.Idle);
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => sut.UpdateTaskStatus(task.Id, "Done", CancellationToken.None));
|
||||||
|
Assert.Contains("not settable externally", ex.Message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~ExternalMcpServiceTests.UpdateTaskStatus"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `UpdateTaskStatus_Cancelled_CancelsAnIdleTask` FAILS with
|
||||||
|
`Status 'Cancelled' is not settable externally.`; `UpdateTaskStatus_Done_StillRejected` passes.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the `Cancelled` branch**
|
||||||
|
|
||||||
|
In `ExternalMcpService.UpdateTaskStatus`, insert between the `Queued` case and `default`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
case TaskStatus.Cancelled:
|
||||||
|
var cancelResult = await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken);
|
||||||
|
if (!cancelResult.Ok)
|
||||||
|
throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update the `[McpServerTool, Description(...)]` text directly above the method — it currently
|
||||||
|
claims only Idle and Queued are permitted. Replace the whole attribute with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Update a task's status. Only 'Idle', 'Queued' and 'Cancelled' are permitted externally — " +
|
||||||
|
"use run_task_now for execution control, and review_task to act on a WaitingForReview task. " +
|
||||||
|
"Settable: Idle (reset to editable), Queued (enqueue for execution), " +
|
||||||
|
"Cancelled (retire the task without deleting it; it can be reset to Idle later). " +
|
||||||
|
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.")]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also fix the `default` branch message, which still points at `cancel_task`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Status '{target}' is not settable externally. Use run_task_now or review_task.");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests to verify they pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~ExternalMcpServiceTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the MCP schema test**
|
||||||
|
|
||||||
|
`ExternalMcpToolSchemaTests` asserts over tool descriptions and may pin the old text.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~ExternalMcpToolSchemaTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS. If it fails on the changed description, update the assertion to match the new
|
||||||
|
text — do not revert the description.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -- src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||||
|
git commit -m "feat(worker): allow update_task_status to set Cancelled" -- src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
(If Step 5 required a schema-test edit, add that path to both commands too.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Five-phase helper prompt
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/PromptFiles.cs:231-276` (`MergeHelperDefault`, `MergeHelperInitialDefault`)
|
||||||
|
- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs:54-80`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Replace the existing `DefaultFor_merge_helper_is_non_empty_and_mentions_the_merge_tools` test with
|
||||||
|
the two below, and keep the other merge-helper tests as they are.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_merge_helper_covers_all_five_phases()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.MergeHelper);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(d));
|
||||||
|
Assert.Contains("Phase 0", d);
|
||||||
|
Assert.Contains("Phase 1", d);
|
||||||
|
Assert.Contains("Phase 2", d);
|
||||||
|
Assert.Contains("Phase 3", d);
|
||||||
|
Assert.Contains("Phase 4", d);
|
||||||
|
Assert.Contains("Phase 5", d);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_merge_helper_names_the_tools_each_phase_needs()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.MergeHelper);
|
||||||
|
Assert.Contains("batch_get_tasks", d); // phase 0
|
||||||
|
Assert.Contains("update_task", d); // phase 1 + 2
|
||||||
|
Assert.Contains("get_app_settings", d); // phase 3
|
||||||
|
Assert.Contains("update_task_status", d); // phase 3
|
||||||
|
Assert.Contains("review_task", d); // phase 4
|
||||||
|
Assert.Contains("continue_merge", d); // phase 4
|
||||||
|
Assert.DoesNotContain("run_task_now(", d); // single override slot — must not batch-start
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~PromptFilesTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both new tests FAIL (no "Phase 0", no `batch_get_tasks`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace `MergeHelperDefault`**
|
||||||
|
|
||||||
|
Replace the whole `private const string MergeHelperDefault = """ … """;` block with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private const string MergeHelperDefault = """
|
||||||
|
You are the ClaudeDo list handler, running as an interactive session with the user watching. Ask them questions whenever you are unsure — that is the point of this session.
|
||||||
|
|
||||||
|
Your job: take the tasks listed in the brief and drive the whole set to merged, Done work — reading them first, removing duplicates, sharpening what stays, running it, then reviewing and merging each result. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
||||||
|
|
||||||
|
Work the five phases in order. Do not start a phase before the previous one is finished.
|
||||||
|
|
||||||
|
## Phase 0 — Read everything
|
||||||
|
Call batch_get_tasks with every id from the brief and read each task's title, description, status and parent/child links. Do not act on any single task before you have read them all — Phase 1 needs the whole set in view.
|
||||||
|
|
||||||
|
## Phase 1 — Dedupe
|
||||||
|
Compare the tasks pairwise for overlap: same goal stated twice, one task fully contained in another, two tasks that would edit the same thing for the same reason.
|
||||||
|
|
||||||
|
Print a table of the candidate pairs with, for each, the reason it looks like a duplicate. Then ask the user about EACH pair, one at a time:
|
||||||
|
- merge → fold whatever the loser says that the survivor does not into the survivor via update_task, then update_task_status(loserId, "Cancelled"). Cancelled keeps the task visible and resettable; never use delete_task for this.
|
||||||
|
- keep both → note why and move on.
|
||||||
|
|
||||||
|
Cancel nothing without an explicit answer. If there are no duplicates, say so and go on.
|
||||||
|
|
||||||
|
## Phase 2 — Enhance for execution
|
||||||
|
Each surviving task is about to be run by an autonomous agent with no further input. Sharpen it so that run can succeed. For each task, rewrite title and description to carry:
|
||||||
|
- concrete acceptance criteria — what must be true when it is done,
|
||||||
|
- the files and areas actually involved, found with Read/Grep/Glob in the repo. Do not guess paths; look them up.
|
||||||
|
- what is explicitly out of scope.
|
||||||
|
|
||||||
|
Write it back with update_task (title, description and commitType are the settable fields).
|
||||||
|
|
||||||
|
Rules: do not change what the user asked for, and do not invent requirements. You are making the existing intent precise, not adding to it. If a task is too vague to sharpen without guessing, ASK instead of guessing. Report a short before/after per task.
|
||||||
|
|
||||||
|
## Phase 3 — Run
|
||||||
|
Do NOT use run_task_now for a batch — there is a single override slot and the second call fails with "override slot busy".
|
||||||
|
|
||||||
|
Read get_app_settings and tell the user how many parallel execution slots are configured (maxParallelExecutions). If it is 1, say plainly that the tasks will execute one after another and that the value is changeable in ClaudeDo's settings.
|
||||||
|
|
||||||
|
Then, for each surviving task:
|
||||||
|
- Idle or Failed → update_task_status(id, "Queued"). For a Failed task ask first whether to reset_failed_task and re-queue it, or skip it.
|
||||||
|
- Queued → leave it; it is already waiting for a slot.
|
||||||
|
- Running or WaitingForChildren → leave it; only poll.
|
||||||
|
- WaitingForReview → leave it; it goes straight to Phase 4.
|
||||||
|
|
||||||
|
Poll get_task until every task has left Queued and Running — WaitingForReview on success, Failed on error. Report progress as tasks land; do not poll silently for minutes.
|
||||||
|
|
||||||
|
## Phase 4 — Review and merge
|
||||||
|
One task at a time, in the order the brief lists them.
|
||||||
|
|
||||||
|
1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title and description.
|
||||||
|
2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
|
||||||
|
3. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||||
|
- Clean merge → the task is Done; move on.
|
||||||
|
- Conflict (markers left in the working tree, repoPath returned) → resolve it.
|
||||||
|
|
||||||
|
Every branch in this run forked from the same base, so conflicts between them are the NORMAL case, not a failure. Resolve them and keep going; do not abandon the run because a merge conflicted.
|
||||||
|
|
||||||
|
Resolving a conflict:
|
||||||
|
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you — fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
|
||||||
|
- For a task WITH children (a unit merge), pass the PARENT task id to continue_merge / abort_merge.
|
||||||
|
- If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing.
|
||||||
|
- Prefer the MCP tools whenever they apply. Only if the MCP tools cannot reach an in-progress merge may you finish it by hand: resolve the markers, then `git add -- <the resolved paths>` and `git commit` — NEVER `git add -A` or a bare commit, because the checkout is shared with other sessions.
|
||||||
|
|
||||||
|
Rules for the whole session:
|
||||||
|
- Never use raw `git merge`, `git reset`, or `git checkout` to force a merge. Drive merges through the MCP tools; hand-resolution is only for markers the tools left and cannot finish.
|
||||||
|
- Ask the user for anything ambiguous, risky, or destructive.
|
||||||
|
|
||||||
|
## Phase 5 — Summary
|
||||||
|
Print one line per task from the original brief:
|
||||||
|
title — dedupe action (kept / merged into X / cancelled as duplicate of X) — enhanced (yes/no) — final status — merge commit (if any) — conflicts resolved (if any).
|
||||||
|
|
||||||
|
Then list anything you skipped or left for the user and why, and any follow-ups worth turning into new tasks.
|
||||||
|
""";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Replace `MergeHelperInitialDefault`**
|
||||||
|
|
||||||
|
The scope is now always one list with one repo, so the header states it once and the task lines
|
||||||
|
drop the constant `list:` / `repo:` fields.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private const string MergeHelperInitialDefault = """
|
||||||
|
# List handler brief
|
||||||
|
|
||||||
|
Scope: {scope}
|
||||||
|
Repo: {repo}
|
||||||
|
|
||||||
|
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
||||||
|
|
||||||
|
{tasks}
|
||||||
|
|
||||||
|
When every task is handled, print the summary.
|
||||||
|
""";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the `{repo}` token test**
|
||||||
|
|
||||||
|
`{repo}` is a new token — Task 4 will pass it. Add to `PromptFilesTests`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_merge_helper_initial_has_repo_token()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial);
|
||||||
|
Assert.Contains("{repo}", d);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing `RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks` test passes only
|
||||||
|
`scope` and `tasks`. `RenderTemplate` leaves unknown tokens alone, so its two `Assert.Contains`
|
||||||
|
still hold and its `Assert.DoesNotContain("{scope}", outp)` still holds. Leave it unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the tests to verify they pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~PromptFilesTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -- src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||||
|
git commit -m "feat(data): five-phase list-handler prompt with dedupe and enhance" -- src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Drop the global entry point and the LIST column
|
||||||
|
|
||||||
|
This removes every caller that passes a null `listId`, clearing the way for Task 4's signature
|
||||||
|
sweep. Types stay nullable here; only callers and UI go.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:103-113`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml:184,206-210`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs:14-53`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml:41-72`
|
||||||
|
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `src/ClaudeDo.Localization/locales/de.json`
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update the dialog tests to the list-only API**
|
||||||
|
|
||||||
|
`Configure` becomes `Configure(string listId, string listName)` and `IsGlobal` and `ListName` are
|
||||||
|
gone. Rewrite the affected tests. `Load_ExcludesTerminalStatuses_AndTicksActionableByDefault`,
|
||||||
|
`CanConfirm_FollowsRowSelection` and `Confirm_ReturnsSelectedIds_InRowOrder` all used
|
||||||
|
`Configure(null, null)` to see every seeded task — point them at `"L1"` instead, which holds all
|
||||||
|
eight seeded statuses (`t-other-list` lives in `L2` and drops out).
|
||||||
|
|
||||||
|
Replace the four tests below; leave `Load_NoCandidates_HasTasksFalse_CannotConfirm` untouched.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_ExcludesTerminalStatuses_AndTicksActionableByDefault()
|
||||||
|
{
|
||||||
|
await SeedAllStatusesAsync();
|
||||||
|
var vm = BuildVm();
|
||||||
|
vm.Configure("L1", "Work");
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.DoesNotContain(vm.Tasks, t => t.Id is "t-done" or "t-cancelled");
|
||||||
|
Assert.DoesNotContain(vm.Tasks, t => t.Id == "t-other-list");
|
||||||
|
Assert.Equal(6, vm.Tasks.Count);
|
||||||
|
|
||||||
|
Assert.True(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
|
||||||
|
Assert.True(vm.Tasks.Single(t => t.Id == "t-queued").IsSelected);
|
||||||
|
Assert.True(vm.Tasks.Single(t => t.Id == "t-review").IsSelected);
|
||||||
|
Assert.True(vm.Tasks.Single(t => t.Id == "t-failed").IsSelected);
|
||||||
|
Assert.False(vm.Tasks.Single(t => t.Id == "t-running").IsSelected);
|
||||||
|
Assert.False(vm.Tasks.Single(t => t.Id == "t-children").IsSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Load_PerListScope_FiltersToThatList()
|
||||||
|
{
|
||||||
|
await SeedAllStatusesAsync();
|
||||||
|
var vm = BuildVm();
|
||||||
|
vm.Configure("L2", "Home");
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.Single(vm.Tasks);
|
||||||
|
Assert.Equal("t-other-list", vm.Tasks[0].Id);
|
||||||
|
Assert.Contains("Home", vm.ScopeLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanConfirm_FollowsRowSelection()
|
||||||
|
{
|
||||||
|
await SeedAllStatusesAsync();
|
||||||
|
var vm = BuildVm();
|
||||||
|
vm.Configure("L1", "Work");
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.True(vm.CanConfirm);
|
||||||
|
|
||||||
|
vm.SelectNoneCommand.Execute(null);
|
||||||
|
Assert.False(vm.CanConfirm);
|
||||||
|
Assert.All(vm.Tasks, t => Assert.False(t.IsSelected));
|
||||||
|
|
||||||
|
vm.Tasks[0].IsSelected = true; // single row re-enables via PropertyChanged hook
|
||||||
|
Assert.True(vm.CanConfirm);
|
||||||
|
|
||||||
|
vm.SelectAllCommand.Execute(null);
|
||||||
|
Assert.All(vm.Tasks, t => Assert.True(t.IsSelected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Confirm_ReturnsSelectedIds_InRowOrder()
|
||||||
|
{
|
||||||
|
await SeedAllStatusesAsync();
|
||||||
|
var vm = BuildVm();
|
||||||
|
vm.Configure("L1", "Work");
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
vm.SelectNoneCommand.Execute(null);
|
||||||
|
vm.Tasks.Single(t => t.Id == "t-review").IsSelected = true;
|
||||||
|
vm.Tasks.Single(t => t.Id == "t-idle").IsSelected = true;
|
||||||
|
|
||||||
|
var closed = false;
|
||||||
|
vm.CloseAction = () => closed = true;
|
||||||
|
vm.ConfirmCommand.Execute(null);
|
||||||
|
|
||||||
|
var result = await vm.Result.Task;
|
||||||
|
Assert.NotNull(result);
|
||||||
|
// Row order (SortOrder): t-idle was seeded before t-review.
|
||||||
|
Assert.Equal(new[] { "t-idle", "t-review" }, result);
|
||||||
|
Assert.True(closed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also change `Cancel_ReturnsNull`'s `vm.Configure(null, null);` to `vm.Configure("L1", "Work");`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~MergeHelperSelectionModalViewModelTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL — the project does not compile, because `Configure(string, string)` does not exist
|
||||||
|
yet and `IsGlobal` was removed from an assertion that still compiles against it. Compilation
|
||||||
|
failure is the expected "red" here.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Make the dialog VM list-only**
|
||||||
|
|
||||||
|
In `MergeHelperSelectionModalViewModel.cs`:
|
||||||
|
|
||||||
|
Remove the `ListName` property from `MergeHelperTaskRowViewModel`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public required string StatusText { get; init; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the field to non-nullable, drop `IsGlobal`, and make `Configure` list-only:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private string _listId = "";
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private string _scopeLabel = "";
|
||||||
|
|
||||||
|
public bool HasTasks => Tasks.Count > 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void Configure(string listId, string listName)
|
||||||
|
{
|
||||||
|
_listId = listId;
|
||||||
|
ScopeLabel = Loc.T("modals.mergeHelper.scopeList", listName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `LoadAsync`, the list filter is now unconditional and `ListName` is no longer selected:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var candidates = await ctx.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled)
|
||||||
|
.Where(t => t.ListId == _listId)
|
||||||
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.Select(t => new { t.Id, t.Title, t.Status })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
foreach (var c in candidates)
|
||||||
|
{
|
||||||
|
var row = new MergeHelperTaskRowViewModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
Title = c.Title,
|
||||||
|
StatusText = c.Status.ToString(),
|
||||||
|
IsSelected = IsTickedByDefault(c.Status),
|
||||||
|
};
|
||||||
|
row.PropertyChanged += OnRowChanged;
|
||||||
|
Tasks.Add(row);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Drop the LIST column from the dialog view**
|
||||||
|
|
||||||
|
In `MergeHelperSelectionModal.axaml`, change both `ColumnDefinitions="32,*,120,120"` (lines 41 and
|
||||||
|
57) to `ColumnDefinitions="32,*,120"`, and delete the two `Grid.Column="3"` elements — the header
|
||||||
|
`TextBlock` bound to `modals.mergeHelper.columnList` (lines 45-46) and the row `TextBlock` bound to
|
||||||
|
`ListName` (lines 68-71).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Remove the global command and the Broom button**
|
||||||
|
|
||||||
|
In `ListsIslandViewModel.cs`, delete the whole `LetClaudeHandleAllAsync` method including its
|
||||||
|
`[RelayCommand]` attribute (lines 103-113). Leave `LetClaudeHandleListAsync` and the
|
||||||
|
`MergeHelperRequest` record as they are — Task 4 changes those.
|
||||||
|
|
||||||
|
In `ListsIslandView.axaml`, revert the button row to two columns:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- New list + import row -->
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
|
||||||
|
```
|
||||||
|
|
||||||
|
and delete the whole `<Button Grid.Column="2" … LetClaudeHandleAllCommand … />` element
|
||||||
|
(lines 206-210) including its `<PathIcon>` child.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Remove the three dead localization keys**
|
||||||
|
|
||||||
|
Delete from **both** `src/ClaudeDo.Localization/locales/en.json` and
|
||||||
|
`src/ClaudeDo.Localization/locales/de.json`:
|
||||||
|
|
||||||
|
- `lists.letClaudeAllTip` (and the trailing comma on the preceding key, so the object stays valid JSON)
|
||||||
|
- `modals.mergeHelper.scopeAll`
|
||||||
|
- `modals.mergeHelper.columnList` (and the trailing comma on the preceding key)
|
||||||
|
|
||||||
|
Keep `lists.contextLetClaude` and `modals.mergeHelper.scopeList`.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Build and run the tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: build succeeds, all tests pass. The localization parity test is the one that catches a
|
||||||
|
key removed from only one of the two JSON files.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -- src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs
|
||||||
|
git commit -m "refactor(ui): scope \"Let Claude handle it\" to a single list" -- src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Non-nullable `listId` and a single-repo launch spec
|
||||||
|
|
||||||
|
One atomic commit across Worker and Ui. Splitting it would leave one side passing `string?` into a
|
||||||
|
`string` parameter — nullable warnings strewn across a commit boundary, and a launch spec that
|
||||||
|
still carries a dead multi-repo path.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs:36`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs:166-253`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:682-687`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:90`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs:525-526`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs:325-357`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:20`
|
||||||
|
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs:103`
|
||||||
|
- Modify: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:78`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs:363-472`
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs:409-463`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite the launch-spec tests**
|
||||||
|
|
||||||
|
In `InteractiveLaunchSpecServiceTests.cs`, replace the four merge-helper `[Fact]`s (from
|
||||||
|
`BuildForMergeHelperAsync_EmptyTaskIds_ThrowsInvalidOperation` through
|
||||||
|
`BuildForMergeHelperAsync_WithListId_UsesListWorkingDirAsCwdAndListScope`) with these five. Keep
|
||||||
|
the `_mergeHelperSessionDirs` field and `TrackSessionDir` helper above them exactly as they are.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_EmptyTaskIds_ThrowsInvalidOperation()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||||
|
var svc = BuildService();
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_ListWithoutExistingWorkingDir_ThrowsInvalidOperation()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync(workingDir: Path.Combine(_tempDir, "gone"));
|
||||||
|
var taskId = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(taskId, listId, TaskStatus.WaitingForReview);
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, CancellationToken.None));
|
||||||
|
Assert.Contains("working directory", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_UnknownList_Throws()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||||
|
var taskId = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
await Assert.ThrowsAsync<KeyNotFoundException>(
|
||||||
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_BuildsListScopedSpecWithSingleRepo()
|
||||||
|
{
|
||||||
|
var repo = Path.Combine(_tempDir, "repoOnly");
|
||||||
|
Directory.CreateDirectory(repo);
|
||||||
|
|
||||||
|
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||||
|
var t1 = Guid.NewGuid().ToString();
|
||||||
|
var t2 = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||||
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
||||||
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
|
Assert.Equal(repo, spec.Cwd);
|
||||||
|
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||||
|
|
||||||
|
var args = spec.Args.ToList();
|
||||||
|
|
||||||
|
var pmIdx = args.IndexOf("--permission-mode");
|
||||||
|
Assert.True(pmIdx >= 0);
|
||||||
|
Assert.Equal("default", args[pmIdx + 1]);
|
||||||
|
|
||||||
|
var atIdx = args.IndexOf("--allowedTools");
|
||||||
|
Assert.Equal("mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill", args[atIdx + 1]);
|
||||||
|
|
||||||
|
// --add-dir: session dir + the list's single repo dir
|
||||||
|
var addIdx = args.IndexOf("--add-dir");
|
||||||
|
var appendIdx = args.IndexOf("--append-system-prompt-file");
|
||||||
|
var addDirs = args.GetRange(addIdx + 1, appendIdx - addIdx - 1);
|
||||||
|
Assert.Equal(new[] { sessionDir, repo }, addDirs);
|
||||||
|
|
||||||
|
var systemPromptPath = args[appendIdx + 1];
|
||||||
|
Assert.Equal(Path.Combine(sessionDir, "system-prompt.md"), systemPromptPath);
|
||||||
|
Assert.True(File.Exists(systemPromptPath));
|
||||||
|
|
||||||
|
// kickoff is the LAST arg (positional), single line, points at brief.md
|
||||||
|
var kickoff = args[^1];
|
||||||
|
var briefPath = Path.Combine(sessionDir, "brief.md");
|
||||||
|
Assert.Contains(briefPath, kickoff);
|
||||||
|
Assert.DoesNotContain('\n', kickoff);
|
||||||
|
|
||||||
|
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildForMergeHelperAsync_BriefNamesListRepoAndEveryTask()
|
||||||
|
{
|
||||||
|
var repo = Path.Combine(_tempDir, "repoBrief");
|
||||||
|
Directory.CreateDirectory(repo);
|
||||||
|
|
||||||
|
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||||
|
var t1 = Guid.NewGuid().ToString();
|
||||||
|
var t2 = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||||
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
||||||
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
|
Assert.Contains("Scope: List: Alpha", brief);
|
||||||
|
Assert.Contains($"Repo: {repo}", brief);
|
||||||
|
Assert.Contains("First task", brief);
|
||||||
|
Assert.Contains("Second task", brief);
|
||||||
|
Assert.Contains(t1, brief);
|
||||||
|
Assert.Contains(t2, brief);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the launch-spec tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~InteractiveLaunchSpecServiceTests.BuildForMergeHelper"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL. `BuildForMergeHelperAsync_UnknownList_Throws` fails because the current code
|
||||||
|
resolves the repo from the tasks and never validates the list; the brief test fails on the missing
|
||||||
|
`Repo:` line.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rewrite `BuildForMergeHelperAsync`**
|
||||||
|
|
||||||
|
Replace the method body (`InteractiveLaunchSpecService.cs:166-253`) with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (taskIds.Count == 0)
|
||||||
|
throw new InvalidOperationException("No tasks selected for the list handler.");
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var taskRepo = new TaskRepository(ctx);
|
||||||
|
var listRepo = new ListRepository(ctx);
|
||||||
|
|
||||||
|
var list = await listRepo.GetByIdAsync(listId, ct)
|
||||||
|
?? throw new KeyNotFoundException($"List not found: {listId}");
|
||||||
|
|
||||||
|
var repoDir = list.WorkingDir;
|
||||||
|
if (string.IsNullOrEmpty(repoDir) || !Directory.Exists(repoDir))
|
||||||
|
throw new InvalidOperationException($"list '{list.Name}' has no existing working directory");
|
||||||
|
|
||||||
|
var briefLines = new List<string>();
|
||||||
|
foreach (var id in taskIds)
|
||||||
|
{
|
||||||
|
var task = await taskRepo.GetByIdAsync(id, ct)
|
||||||
|
?? throw new KeyNotFoundException($"Task not found: {id}");
|
||||||
|
briefLines.Add($"- [{task.Status}] {task.Title} (id: {task.Id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionDir = Path.Combine(Paths.AppDataRoot(), "merge-helper-sessions", Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(sessionDir);
|
||||||
|
|
||||||
|
var systemPromptPath = Path.Combine(sessionDir, "system-prompt.md");
|
||||||
|
await File.WriteAllTextAsync(systemPromptPath, PromptFiles.ReadOrDefault(PromptKind.MergeHelper), ct);
|
||||||
|
|
||||||
|
var briefPath = Path.Combine(sessionDir, "brief.md");
|
||||||
|
await File.WriteAllTextAsync(briefPath, PromptFiles.Render(PromptKind.MergeHelperInitial,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["scope"] = $"List: {list.Name}",
|
||||||
|
["repo"] = repoDir,
|
||||||
|
["tasks"] = string.Join("\n", briefLines),
|
||||||
|
}), ct);
|
||||||
|
|
||||||
|
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||||
|
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||||
|
|
||||||
|
// Mirrors WindowsTerminalLauncher.BuildPlanningStartArgs ordering: variadic flags
|
||||||
|
// (--allowedTools, --add-dir) first, then a single-value flag, then the single-line
|
||||||
|
// positional kickoff LAST — a multi-line positional prompt truncates at the first
|
||||||
|
// newline, so the full multi-line brief travels via the file exposed through --add-dir.
|
||||||
|
var args = new List<string>
|
||||||
|
{
|
||||||
|
"--permission-mode", "default",
|
||||||
|
"--allowedTools", MergeHelperAllowedTools,
|
||||||
|
"--add-dir", sessionDir, repoDir,
|
||||||
|
"--append-system-prompt-file", systemPromptPath,
|
||||||
|
$"Read the file {briefPath} first. It lists the tasks you must handle and their status. " +
|
||||||
|
"After reading it, begin the session as your instructions describe.",
|
||||||
|
};
|
||||||
|
|
||||||
|
var env = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["MCP_TOOL_TIMEOUT"] = "200000",
|
||||||
|
};
|
||||||
|
|
||||||
|
return new LaunchSpec(cwd: repoDir, resolvedClaude, args, env);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Flip the interface and hub signatures**
|
||||||
|
|
||||||
|
`src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs:36`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/ClaudeDo.Worker/Hub/WorkerHub.cs:682`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId) => HubGuard(() =>
|
||||||
|
```
|
||||||
|
|
||||||
|
(leave the method body as it is).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the Worker tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||||
|
--filter "FullyQualifiedName~InteractiveLaunchSpecServiceTests"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: build succeeds, all pass. `TasksIslandViewModelPlanningTests.cs:78` holds a fake
|
||||||
|
implementing `IWorkerClient` — its signature is flipped in Step 7; if the Worker.Tests build fails
|
||||||
|
there, do Step 7 first and re-run.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update the Mission Control tests**
|
||||||
|
|
||||||
|
In `MissionControlViewModelTests.cs`, the four `OpenMergeHelperConPtySessionAsync` calls pass
|
||||||
|
`null` as the list id. Replace `null` with `"L1"` on lines 421, 435, 436 and 461, and change the
|
||||||
|
`ThrowingMergeHelperLaunchSpecWorker` override signature at line 411 to:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||||
|
```
|
||||||
|
|
||||||
|
The list-title lookup in `OpenMergeHelperConPtySessionAsync` is wrapped in a `try/catch` and falls
|
||||||
|
back to the plain title, so an unseeded `"L1"` is harmless.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Flip the UI signatures**
|
||||||
|
|
||||||
|
`src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:90`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/ClaudeDo.Ui/Services/WorkerClient.cs:525`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs:103` and
|
||||||
|
`tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:78` — same parameter change
|
||||||
|
(`string? listId` → `string listId`), bodies unchanged.
|
||||||
|
|
||||||
|
`src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:20`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>Confirmed handler run: the scope list and the ordered selected task ids.</summary>
|
||||||
|
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds);
|
||||||
|
```
|
||||||
|
|
||||||
|
`src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs:325-341` — the `listId is not null` guard is
|
||||||
|
now dead:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// List-handler session over a hand-picked set of tasks ("Let Claude handle it").
|
||||||
|
// Ad-hoc style: no owning task, never deduped — every run opens a fresh pane.
|
||||||
|
public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
||||||
|
{
|
||||||
|
if (taskIds is not { Count: > 0 }) return;
|
||||||
|
|
||||||
|
var title = Loc.T("missionControl.mergeHelperTitle");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId);
|
||||||
|
if (list?.Name is { Length: > 0 } name) title = $"{title} — {name}";
|
||||||
|
}
|
||||||
|
catch { /* best-effort title lookup */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave the rest of the method (the `try` block that fetches the spec and adds the pane) unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Build everything and run the full suite**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both builds succeed with no `CS8600`/`CS8604` nullability warnings on the touched files,
|
||||||
|
and every test passes.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -- src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs
|
||||||
|
git commit -m "refactor(worker): make the list-handler launch spec single-list and single-repo" -- src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Update the project docs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/CLAUDE.md`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/CLAUDE.md`
|
||||||
|
- Modify: `docs/open.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check what the CLAUDE.md files claim**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "merge.helper\|Let Claude handle\|MergeHelper" src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Correct any stale claim**
|
||||||
|
|
||||||
|
Where those files describe the merge helper as having a global scope, or describe the prompt as
|
||||||
|
run-and-merge only, update them to: list-scoped only, single repo, five phases (read, dedupe,
|
||||||
|
enhance, queue, review+merge). Note in `src/ClaudeDo.Worker/CLAUDE.md` that
|
||||||
|
`update_task_status` now also accepts `Cancelled`. Do not restructure the files beyond that.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the open verification items**
|
||||||
|
|
||||||
|
Append to the open-items section of `docs/open.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- **List handler (2026-07-27)** — visual pass: the Broom button is gone from the lists footer,
|
||||||
|
the context-menu item appears only on lists with a working dir, and the selection dialog has no
|
||||||
|
LIST column. Plus a real-Claude smoke run of the five phases (dedupe questions, enhancements
|
||||||
|
landing in task descriptions, queued execution, merges).
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -- src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||||
|
git commit -m "docs: describe the list-scoped five-phase handler" -- src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification left to the user
|
||||||
|
|
||||||
|
None of this can be confirmed from tests alone:
|
||||||
|
|
||||||
|
- The lists footer no longer shows the Broom button, and the row context menu still offers
|
||||||
|
"Let Claude handle it" for lists with a working dir.
|
||||||
|
- The selection dialog shows TASK and STATUS only, and the scope line reads `List: <name>`.
|
||||||
|
- A real ConPTY run: Phase 1 asks about duplicates, Phase 2's enhancements are visible in the task
|
||||||
|
descriptions afterwards, Phase 3 reports the slot count and the tasks execute, Phase 4 merges or
|
||||||
|
hands off to conflict resolution.
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Session Skills — Design
|
||||||
|
|
||||||
|
**Date:** 2026-07-03
|
||||||
|
**Status:** Approved (design), implementation not started
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Mika wants to give headless task agents a specific Claude skill (e.g.
|
||||||
|
[ponytail](https://github.com/DietrichGebert/ponytail)) **without** installing it
|
||||||
|
globally in `~/.claude/skills/`, where it would leak into every interactive session.
|
||||||
|
Skills should be a first-class, per-level session setting alongside `model`,
|
||||||
|
`max_turns`, and `system_prompt` — configurable **global / per-list / per-task** — and
|
||||||
|
sourced from a GitHub URL.
|
||||||
|
|
||||||
|
## Key facts that shape the design
|
||||||
|
|
||||||
|
- The Claude CLI discovers skills from two places: the **global** `~/.claude/skills/`
|
||||||
|
(every session — undesirable here) and the **working directory's** `.claude/skills/`
|
||||||
|
(plus plugins). ClaudeDo fully controls each spawned session's `WorkingDirectory`
|
||||||
|
(`ClaudeProcess.cs:30`), so a skill dropped into the session cwd is scoped to exactly
|
||||||
|
that headless run.
|
||||||
|
- **Auto-commit uses `git add -A`** (`WorktreeManager.CommitIfChangedAsync` →
|
||||||
|
`_git.AddAllAsync`, `WorktreeManager.cs:142`). Anything seeded into a worktree's
|
||||||
|
`.claude/skills/` would be committed unless explicitly excluded → the seeder must add
|
||||||
|
the seeded paths to the worktree's `info/exclude`.
|
||||||
|
- A skill is not just prompt text: `SKILL.md` may reference scripts that run via Bash.
|
||||||
|
Headless agents run with `--permission-mode auto` (effectively unattended), so a skill
|
||||||
|
pulled from an arbitrary URL is **unattended third-party code execution**. This is why
|
||||||
|
install is a deliberate, pinned, reviewable step — not a live per-run URL fetch.
|
||||||
|
|
||||||
|
## Decisions (locked)
|
||||||
|
|
||||||
|
1. **Install-and-pin, not live fetch.** A dedicated registry screen installs a skill
|
||||||
|
once: clone the repo, pin to the current commit, store locally. Per-level config then
|
||||||
|
references installed skills **by name** (checkboxes), never a URL.
|
||||||
|
2. **Three levels, additive union.** Effective skill set = `global ∪ list ∪ task`.
|
||||||
|
(Unlike `model`/`prompt`, which override — skills add up. Trade-off accepted: an
|
||||||
|
inherited skill can't be switched off for a single task in the MVP.)
|
||||||
|
3. **A repo can contribute multiple skills.** Installer detects the layout:
|
||||||
|
- `skills/*/SKILL.md` (plugin bundle) → import **each** subskill flat. This is
|
||||||
|
ponytail: it ships 6 skills (`ponytail`, `-help`, `-review`, `-audit`, `-debt`,
|
||||||
|
`-gain`) under `skills/<name>/SKILL.md` plus `.claude-plugin/`, hooks, commands, an
|
||||||
|
MCP — none of which we consume; we take only the `skills/<name>/` dirs.
|
||||||
|
- root `SKILL.md` → single skill.
|
||||||
|
- neither → reject.
|
||||||
|
The CLI expects `.claude/skills/<name>/SKILL.md` **flat**, so subskills are flattened
|
||||||
|
on install. Selection is **per individual skill name** (enable just `ponytail` +
|
||||||
|
`ponytail-help` if you want, not all six).
|
||||||
|
4. **Public repos only** (plain `git clone` over HTTPS, no auth) for MVP.
|
||||||
|
|
||||||
|
> **Correction (2026-07-03, from the smoke test):** the original "one repo = one skill
|
||||||
|
> at root" MVP was wrong for the very target repo — ponytail is a multi-skill plugin.
|
||||||
|
> Decision 3 above replaces it.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Storage & registry
|
||||||
|
|
||||||
|
- Each discovered skill is copied **flat** to `~/.todo-app/session-skills/<name>/` (its
|
||||||
|
own self-contained dir with `SKILL.md` at the root of that dir), so the seeder just
|
||||||
|
copies `<name>/` → cwd.
|
||||||
|
- New DB table `session_skills`, one row **per skill** (a multi-skill repo writes N rows
|
||||||
|
sharing `source_url` + `pinned_ref`): `name` (PK), `source_url`, `pinned_ref` (commit
|
||||||
|
SHA), `subpath` (dir within the repo the skill came from, e.g. `skills/ponytail` or
|
||||||
|
`.` for root), `description`, `added_at`. Repo-level ops act on all rows with the same
|
||||||
|
`source_url` (no separate sources table — keep it flat).
|
||||||
|
- New worker service `SessionSkillRegistry` (in a new `Skills/` area under the Worker):
|
||||||
|
- `InstallAsync(url)` — clone to temp → **detect layout** (`skills/*/SKILL.md` bundle
|
||||||
|
vs root `SKILL.md`) → for each discovered skill parse frontmatter (`name`,
|
||||||
|
`description`), resolve HEAD SHA as `pinned_ref`, copy its dir flat into place, upsert
|
||||||
|
a row. Returns the list of installed skill names. Name collision (a skill name from a
|
||||||
|
*different* source) → error surfaced to UI; reinstalling the same source updates.
|
||||||
|
Clone is injected (`IRepoCloner`) so tests use a local source dir — **no real network,
|
||||||
|
no real CLI**.
|
||||||
|
- `UpdateAsync(sourceUrl)` — re-clone, re-detect, refresh that source's skills +
|
||||||
|
`pinned_ref`.
|
||||||
|
- `RemoveAsync(sourceUrl)` — delete all its skill dirs + rows.
|
||||||
|
- `ListAsync()` — registry entries for the UI (grouped by source for display).
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
`TaskRunner.ResolveConfigAsync` (`TaskRunner.cs:488`) already merges
|
||||||
|
task → list → global for the other fields. Add:
|
||||||
|
|
||||||
|
```
|
||||||
|
SkillNames = Union(task.SessionSkills, listConfig?.SessionSkills, global.SessionSkills)
|
||||||
|
```
|
||||||
|
|
||||||
|
deduped, filtered to names that still exist in the registry (a removed skill is silently
|
||||||
|
dropped — logged). Add `IReadOnlyList<string> SkillNames` to `ClaudeRunConfig`
|
||||||
|
(`ClaudeArgsBuilder.cs:5`). **No CLI flag is emitted** — skills are seeded on disk, not
|
||||||
|
passed as args. `ClaudeArgsBuilder.Build` is unchanged for skills.
|
||||||
|
|
||||||
|
Per-level storage: a nullable TEXT column `session_skills` (JSON array of names) on
|
||||||
|
`tasks`, `list_config`, and `app_settings`.
|
||||||
|
|
||||||
|
### Seeding
|
||||||
|
|
||||||
|
New service `SessionSkillSeeder`, called by `TaskRunner` after the working dir is
|
||||||
|
resolved and before `ClaudeProcess.RunAsync`:
|
||||||
|
|
||||||
|
- For each resolved skill name, copy `~/.todo-app/session-skills/<name>/` →
|
||||||
|
`<cwd>/.claude/skills/<name>/` (overwrite → idempotent for resume/re-run).
|
||||||
|
- If the cwd is a git worktree, append `/.claude/skills/<name>/` to the worktree's
|
||||||
|
`info/exclude` (path via `git rev-parse --git-path info/exclude`, so it targets the
|
||||||
|
per-worktree exclude), only if not already present. **Only the seeded subdirs are
|
||||||
|
excluded** — never blanket-exclude `/.claude/`, in case the target project commits its
|
||||||
|
own `.claude/`.
|
||||||
|
- Sandbox runs (not a repo) skip the exclude step.
|
||||||
|
- No separate cleanup: seeded dirs vanish with the worktree/sandbox.
|
||||||
|
|
||||||
|
### allowedTools caveat
|
||||||
|
|
||||||
|
`--allowedTools` is only emitted when set (`ClaudeArgsBuilder.cs:80`); normal task runs
|
||||||
|
leave it null → all tools allowed → the `Skill` tool is available. If a future per-task
|
||||||
|
allowedTools restriction is added, it must include `Skill`. Noted, not handled in MVP.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
Mirror the existing agent-file pattern.
|
||||||
|
|
||||||
|
- **Registry screen ("extra mask"):** a new **Skills** tab in the Settings modal
|
||||||
|
(`SettingsModalView.axaml`) with `SessionSkillsSettingsTabViewModel`. **Add** (URL text
|
||||||
|
box → install a repo, which may yield several skills); lists installed skills grouped by
|
||||||
|
source (name, description, source, short ref); **Update** / **Remove** act per source
|
||||||
|
(repo). Status/error line like `FilesSettingsTabViewModel`.
|
||||||
|
- **Global selector:** multi-select (checkbox list) of installed skills in the General
|
||||||
|
settings tab → `AppSettings.SessionSkills`.
|
||||||
|
- **List + Task selectors:** add a skills multi-select to the shared
|
||||||
|
`AgentConfigEditor` control (`AgentConfigEditor.axaml` /
|
||||||
|
`AgentConfigEditorViewModel.cs`), which is already reused by both List settings and the
|
||||||
|
per-task flyout — one addition covers both levels, with the existing inheritance-badge
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
### Hub / client surface
|
||||||
|
|
||||||
|
New `WorkerHub` methods + `IWorkerClient` entries (update hand-rolled fakes in both test
|
||||||
|
projects — see memory `iworkerclient_fakes_sync`):
|
||||||
|
`GetSessionSkills`, `InstallSessionSkill(url)`, `UpdateSessionSkill(sourceUrl)`,
|
||||||
|
`RemoveSessionSkill(sourceUrl)`. Extend `AppSettingsDto`, `ListConfigDto`,
|
||||||
|
`UpdateListConfigDto`, `UpdateTaskAgentSettingsDto` with the selected skill-name lists.
|
||||||
|
New `SessionSkillDto`.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
- **Removed skill still referenced** by a level → dropped at resolve time, logged, no
|
||||||
|
failure.
|
||||||
|
- **Name collision on install** → reject with a clear message; offer Update instead.
|
||||||
|
- **Repo without root `SKILL.md`** → reject at install.
|
||||||
|
- **Target project already has `.claude/skills/`** → additive copy; exclude only our
|
||||||
|
subdirs.
|
||||||
|
- **Resume / re-run** reuses the worktree → re-seed overwrites, exclude append is
|
||||||
|
idempotent.
|
||||||
|
|
||||||
|
## Verification (must-check, can't be unit-tested)
|
||||||
|
|
||||||
|
- **Does `claude -p` actually load and invoke a skill placed in cwd `.claude/skills/`?**
|
||||||
|
**CONFIRMED 2026-07-03.** Mika ran, in a stable terminal, a `claude -p` invocation in a
|
||||||
|
scratch cwd holding `.claude/skills/ponytail*/SKILL.md`; the model invoked the
|
||||||
|
`ponytail-help` skill and returned its exact Lite/Full/Ultra table. Headless mode does
|
||||||
|
surface cwd skills → the whole approach holds; no `CLAUDE_CONFIG_DIR` fallback needed.
|
||||||
|
- Seeded skill is **not** committed by the auto-commit step (worktree run).
|
||||||
|
- Skill does not appear in a normal interactive session (no global leak).
|
||||||
|
|
||||||
|
## Out of scope (MVP)
|
||||||
|
|
||||||
|
Private-repo auth; consuming a plugin's *non-skill* parts (hooks, commands, MCP — we take
|
||||||
|
only `skills/<name>/`); auto-update & update notifications; per-task *disabling* of an
|
||||||
|
inherited skill; surfacing skill invocation in the run log.
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# ConPTY Interactive Sessions — Design
|
||||||
|
|
||||||
|
Date: 2026-07-23
|
||||||
|
Status: Approved (design), implementation not started
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The current in-app interactive session path streams `stream-json` from a
|
||||||
|
`claude` process spawned **in the Worker** and renders it as a chat log with a
|
||||||
|
composer. It does not surface permission requests, AskUser questions, and other
|
||||||
|
TUI-native interactions well — the rendering is a partial reimplementation of
|
||||||
|
what the real Claude Code TUI already does. We want full fidelity for
|
||||||
|
interactive work without rebuilding the TUI.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Hybrid execution model:**
|
||||||
|
|
||||||
|
- **Autonomous queue tasks** (`Status=Queued`, picked by the queue): unchanged.
|
||||||
|
Headless `stream-json`, full orchestration (status flow, diff, review, merge).
|
||||||
|
- **Interactive sessions**: an **embedded ConPTY terminal** running the real
|
||||||
|
`claude` CLI, rendered in the **UI process**. Full TUI fidelity (permission
|
||||||
|
prompts, questions, colors, everything the standalone CLI does). Detached from
|
||||||
|
the review/merge/status machinery — these are a manual cockpit.
|
||||||
|
|
||||||
|
This is the third direction for interactive (external `wt` terminal → streaming
|
||||||
|
chat → embedded ConPTY). The streaming interactive stack is **removed**, not run
|
||||||
|
in parallel — accepted as discarded work in exchange for one interactive path
|
||||||
|
and full fidelity.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Process location
|
||||||
|
|
||||||
|
ConPTY terminal controls render in-process and spawn their child (`claude`) as a
|
||||||
|
child of the host process. Therefore interactive sessions move **out of the
|
||||||
|
Worker and into the UI process**. They no longer flow over SignalR. This mirrors
|
||||||
|
the existing `ResumeTaskInTerminal` behavior (launch real `claude`), but embedded
|
||||||
|
instead of via external `wt.exe`.
|
||||||
|
|
||||||
|
### Worktree preparation (task-based sessions)
|
||||||
|
|
||||||
|
Interactive task sessions get the **same worktree preparation as autonomous
|
||||||
|
runs**: session-skills seeding, agent files, MCP config, environment. The Worker
|
||||||
|
performs the prep and returns a launch spec to the UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
LaunchSpec {
|
||||||
|
cwd: string // worktree path
|
||||||
|
exe: string // resolved claude executable / shell
|
||||||
|
args: string[] // e.g. --resume <sessionId>
|
||||||
|
env: Dictionary<string,string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The command construction reuses `WindowsTerminalLauncher.BuildResumeArgs`/`Resolve`.
|
||||||
|
Guards mirror `ResumeTaskInTerminal` for Running/Queued (rejected). Worktree
|
||||||
|
handling (FINAL):
|
||||||
|
- Existing Active/Kept worktree + persisted SessionId → `--resume <id>`.
|
||||||
|
- No usable worktree but the list has a WorkingDir (git repo) → create a worktree
|
||||||
|
**on demand** via `WorktreeManager.CreateAsync` (the same path autonomous runs
|
||||||
|
use), then a fresh-start spec. This lets never-run tasks be opened interactively.
|
||||||
|
- Fresh (non-resume) session → the task's prompt (title + description) is passed
|
||||||
|
as claude's positional prompt so the session starts on the task.
|
||||||
|
- No worktree and no WorkingDir → clear error.
|
||||||
|
Interactive sessions are detached: ClaudeDo does NOT record their claude session
|
||||||
|
id (ConPTY is opaque, no stream-json), so resuming a specific past interactive
|
||||||
|
conversation is only via claude's own `--continue`/`--resume` in the worktree dir.
|
||||||
|
|
||||||
|
### Free / ad-hoc sessions
|
||||||
|
|
||||||
|
In addition to task-based sessions, the user can open an ad-hoc terminal in a
|
||||||
|
chosen directory (no task). These also get MCP config + env set up so the
|
||||||
|
`claudedo` tools are available, but no per-level session-skills seeding tied to a
|
||||||
|
task.
|
||||||
|
|
||||||
|
### Terminal host control — RESOLVED by spike (2026-07-23)
|
||||||
|
|
||||||
|
**Library: `Iciclecreek.Avalonia.Terminal` 2.0.3** (namespace `Iciclecreek.Terminal`).
|
||||||
|
It needs Avalonia >= 12.0.2; the repo is on 12.0.4 → compatible, no bump.
|
||||||
|
`SvcSystems.UI.Terminal` (latest) needs Avalonia 12.1+ → rejected.
|
||||||
|
|
||||||
|
Visual pass (user, 2026-07-23): the real `claude` TUI renders correctly inside
|
||||||
|
the embedded control — Claude Code opened and was usable.
|
||||||
|
|
||||||
|
**Binding approach — use the library's own `LaunchProcess()` (FINAL).**
|
||||||
|
An initial attempt drove `Porta.Pty` ourselves (own read loop, key tunneling via
|
||||||
|
`GenerateKeyInput`/`GenerateCharInput`, manual resize) to bypass
|
||||||
|
`LaunchProcess()` and inject a custom `PtyOptions.Environment`. That was a
|
||||||
|
mistake: it rendered wrong, lagged, and dropped input. The spike had proven the
|
||||||
|
library's own `TerminalControl.LaunchProcess()` pipeline renders correctly, stays
|
||||||
|
responsive, and handles input/resize/focus. So `PtyTerminalSession` is a thin
|
||||||
|
wrapper (`src/ClaudeDo.Ui/Services/PtyTerminalSession.cs`):
|
||||||
|
|
||||||
|
- Set `control.Process = descriptor.Exe`, `control.Args = descriptor.Args`,
|
||||||
|
`control.StartingDirectory = descriptor.Cwd`, then `await control.LaunchProcess()`.
|
||||||
|
- Relay the control's own `ProcessExited` event and `Kill()`.
|
||||||
|
- `Process=""` stays on the AXAML `TerminalControl` to suppress the library's
|
||||||
|
auto-launch-on-load, so exactly one process starts (our manual launch).
|
||||||
|
|
||||||
|
**Environment:** `LaunchProcess()` gives no per-launch env dict, but
|
||||||
|
`Porta.Pty.SpawnAsync` inherits the *current process* environment. So apply
|
||||||
|
`descriptor.Env` entries via `Environment.SetEnvironmentVariable(key, value)`
|
||||||
|
(process scope) before `LaunchProcess()`. The only var needed is
|
||||||
|
`MCP_TOOL_TIMEOUT`; session-skills/agent-files/MCP-config are on disk / globally
|
||||||
|
registered, independent of env. (The earlier "child gets zero env vars" claim was
|
||||||
|
wrong — Porta.Pty seeds from the process env.)
|
||||||
|
|
||||||
|
**Sizing gotcha (critical):** the control derives Cols/Rows from
|
||||||
|
`arranged-size / character-cell-size`. Without an EXPLICIT monospace font the cell
|
||||||
|
metrics are wrong and the child TUI renders into the wrong area. Set
|
||||||
|
`FontFamily="Cascadia Mono,Consolas,monospace"`, `FontSize`, `BufferSize`, and
|
||||||
|
`HorizontalAlignment/VerticalAlignment=Stretch` on the `TerminalControl` (matching
|
||||||
|
the spike) — this is what made rendering correct in the pane.
|
||||||
|
|
||||||
|
**Lesson:** do not hand-roll pty/input/render around this control — use its
|
||||||
|
`LaunchProcess()` pipeline.
|
||||||
|
|
||||||
|
### Command Center layout
|
||||||
|
|
||||||
|
- `MonitorPaneView` keeps the streamed log for autonomous tasks.
|
||||||
|
- Interactive panes host the terminal control instead of the log+composer.
|
||||||
|
- Layout is **toggleable**: focus mode (tabs, one session large) ↔ overview mode
|
||||||
|
(grid, several sessions at once).
|
||||||
|
|
||||||
|
## Removals
|
||||||
|
|
||||||
|
Worker:
|
||||||
|
- `StreamingClaudeSession`, `InteractiveSessionService`
|
||||||
|
- `WorkerHub` interactive methods: `OpenInteractiveTerminal`,
|
||||||
|
`SendInteractiveMessage`, `RemoveQueuedInteractiveMessage`,
|
||||||
|
`StopInteractiveSession`, `InterruptInteractiveSession`
|
||||||
|
- Broadcast events: `InteractiveSessionStarted/Ended`, `InteractiveQueueChanged`,
|
||||||
|
`InteractiveMessageSent`
|
||||||
|
- `IdleSessionReaper` and `LiveSessionRegistry` **iff** unused elsewhere
|
||||||
|
(verify during implementation — do not delete blindly).
|
||||||
|
|
||||||
|
UI:
|
||||||
|
- Composer on `TaskMonitorViewModel`: `ComposerDraft`, `SubmitComposerCommand`,
|
||||||
|
`InterruptInteractiveCommand`, `StopInteractiveCommand`, `QueuedMessages`,
|
||||||
|
`IsInteractiveLive`.
|
||||||
|
- The composer + queued-messages portion of `SessionTerminalView` (the log
|
||||||
|
portion stays for autonomous panes).
|
||||||
|
- `IWorkerClient` interactive methods.
|
||||||
|
|
||||||
|
## Open items (implementation time)
|
||||||
|
|
||||||
|
- Whether `LiveSessionRegistry` is referenced outside the interactive path.
|
||||||
|
- Session-id availability for a never-run task (no `--resume` → start fresh).
|
||||||
|
|
||||||
|
Resolved: env approach (see Terminal host control — add custom vars on top of the
|
||||||
|
inherited process env via `PtyOptions.Environment`); library + binding seam.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No screen-scraping of terminal output back into task status/diff/review.
|
||||||
|
- No change to the autonomous queue execution path.
|
||||||
|
|
||||||
|
## Status (2026-07-23)
|
||||||
|
|
||||||
|
Implemented on main (not pushed) and visually verified by the user (rendering,
|
||||||
|
input, responsiveness correct after switching to `LaunchProcess()` + setting a
|
||||||
|
monospace font). Done: worker launch-spec (task + ad-hoc, on-demand worktree,
|
||||||
|
prompt seeding), UI terminal host, Command Center hosting (grid↔tabs), streaming
|
||||||
|
stack removed. Permission mode: left to claude's default (not forced), per user.
|
||||||
|
|
||||||
|
Deferred: **Avalonia 12.1 upgrade** — blocked, not adopted. 12.1's XAML source
|
||||||
|
generator needs Roslyn 4.14 (.NET 9.0.3xx SDK); the repo pins .NET 8 in
|
||||||
|
`global.json`, and bumping the SDK floor risks the Gitea Actions release build.
|
||||||
|
Staying on Avalonia 12.0.x (Iciclecreek 2.0.3 works there). Revisit only with a
|
||||||
|
deliberate SDK-floor decision.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# ConPTY Planning Sessions — Design (2026-07-24)
|
||||||
|
|
||||||
|
**Task:** `5d627df8` — interaktive Planning-Session über embedded ConPTY statt externem `wt`-Fenster.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Planning-Sessions (`StartPlanningSession`/`ResumePlanningSession`) öffnen heute ein externes
|
||||||
|
Windows-Terminal (`WindowsTerminalLauncher` → `wt.exe`). Seit ConPTY existiert (embedded
|
||||||
|
claude-TUI im UI-Prozess, Command Center), soll die interaktive Planning-Session denselben
|
||||||
|
Weg nutzen: konsistente UX, keine externen Fenster.
|
||||||
|
|
||||||
|
## Approved decisions (Brainstorm 2026-07-24)
|
||||||
|
- **Env-Isolation:** Prozess-global akzeptiert (Sessions werden sequenziell per Klick geöffnet;
|
||||||
|
jedes claude-Child snapshottet Env beim Spawn). Keine Änderung an `PtyTerminalSession`.
|
||||||
|
- **wt-Launcher:** Code bleibt; nur das Routing für Planning wird auf ConPTY umgestellt
|
||||||
|
(kein Rip-out von `LaunchPlanningStart/ResumeAsync`).
|
||||||
|
- Planning-Kachel im Command Center (Mission Control), wie andere ConPTY-Sessions.
|
||||||
|
Finalize/Discard/Queue-Plan bleiben unverändert auf den bestehenden Buttons.
|
||||||
|
|
||||||
|
## Change map
|
||||||
|
|
||||||
|
### Worker
|
||||||
|
1. `WindowsTerminalLauncher`: Planning-Start-Args in einen bare Builder
|
||||||
|
`BuildPlanningStartArgs(PlanningSessionStartContext) -> IReadOnlyList<string>` herausziehen
|
||||||
|
(analog `BuildResumeArgs`); `BuildPlanningStartCommand` nutzt ihn weiter (wt unverändert).
|
||||||
|
Resume-Args `BuildPlanningResumeArgs(sessionId) = ["--permission-mode","plan","--resume",id]`
|
||||||
|
(bisher inline in `LaunchPlanningResumeAsync`).
|
||||||
|
2. `InteractiveLaunchSpecService`: `BuildPlanningStart(PlanningSessionStartContext) -> LaunchSpec`
|
||||||
|
und `BuildPlanningResume(PlanningSessionResumeContext) -> LaunchSpec` — resolve claude,
|
||||||
|
Args aus (1), Env `MAX_THINKING_TOKENS=20000` + `CLAUDEDO_PLANNING_TOKEN=<token>`
|
||||||
|
(+ `MCP_TOOL_TIMEOUT` wie interaktiv), Cwd = ctx.WorkingDir. Nimmt den Kontext (keine
|
||||||
|
Manager-Kopplung).
|
||||||
|
3. Hub: `GetPlanningStartLaunchSpec(taskId) -> LaunchSpec` (ruft `_planning.StartAsync`,
|
||||||
|
broadcastet `TaskUpdated`, baut Spec; bei Fehler discard+rethrow wie heute),
|
||||||
|
`GetPlanningResumeLaunchSpec(taskId) -> LaunchSpec` (ruft `_planning.ResumeAsync`).
|
||||||
|
|
||||||
|
### UI
|
||||||
|
4. `IWorkerClient`/`WorkerClient`: `GetPlanningStartLaunchSpecAsync`/`GetPlanningResumeLaunchSpecAsync`
|
||||||
|
(mirror `GetInteractiveLaunchSpecAsync`).
|
||||||
|
5. `MissionControlViewModel`: `OpenPlanningConPtySessionAsync(taskId, resume)` — dedupt nach
|
||||||
|
TaskId, holt die Planning-Spec, baut `TerminalLaunchDescriptor` → `ConPtyPaneViewModel`
|
||||||
|
(Titel „<task> (Planning)").
|
||||||
|
6. `TasksIslandViewModel`: `OpenPlanningSessionAsync`/`ResumePlanningSessionAsync` (Resume-Zweig
|
||||||
|
der `UnfinishedPlanningModal`) öffnen die Planning-ConPTY-Kachel via neuem Event
|
||||||
|
`OpenPlanningConPtyRequested(taskId, resume)`, statt `StartPlanningSessionAsync`/wt.
|
||||||
|
`IslandsShellViewModel` verdrahtet das Event → `OpenMissionControl()` +
|
||||||
|
`MissionControl.OpenPlanningConPtySessionAsync`.
|
||||||
|
|
||||||
|
### #12 (Nebenbefund)
|
||||||
|
MCP-Permission-Prompt trotz `--allowedTools "mcp__claudedo__*"`: im interaktiven ConPTY vom
|
||||||
|
User bestätigbar (kein Blocker wie headless). Beim Testen prüfen, ob der Glob den Prompt in
|
||||||
|
der aktuellen CLI unterdrückt; falls nicht, allowedTools/`--permission-mode`-Kombi nachziehen.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
- Entfernen des wt-Codes / `ResumeTaskInTerminal`.
|
||||||
|
- Per-Child-Env-Isolation in `PtyTerminalSession`.
|
||||||
|
- Änderungen an Finalize/Discard/Queue-Plan-Lifecycle.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Build Worker + App (`-c Release`), Worker.Tests + Ui.Tests grün.
|
||||||
|
- Visual (User): Planning-Start öffnet Command-Center-Kachel mit claude-TUI im Plan-Modus;
|
||||||
|
create_child_task erzeugt Draft-Kinder live; Resume greift die Session; kein wt-Fenster.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# Merge Helper ("Let Claude handle it") — Design
|
||||||
|
|
||||||
|
**Status:** Proposed — awaiting approval
|
||||||
|
**Date:** 2026-07-24
|
||||||
|
**Scope:** Feature — a per-list and global button that opens an **interactive ConPTY Claude session** pre-loaded with a set of user-selected tasks. The session (the "Merge Helper") drives each selected task to completion and merge autonomously via `mcp__claudedo__*` tools, asks the user interactively (in the ConPTY terminal) only when uncertain, resolves merge conflicts itself, and ends with a written summary of everything that changed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
|
||||||
|
Collapse the repetitive per-task review→merge clicking into a single "Let Claude handle it" action. The user picks the tasks; an embedded Claude session babysits them — running the ones that still need running, reviewing diffs, merging the clean ones, resolving conflicts, and reporting back — while remaining fully interactive so the user can answer questions mid-run.
|
||||||
|
|
||||||
|
This reuses the existing ConPTY infrastructure (UI-process embedded terminal) and the globally-registered `claudedo` MCP server. The only genuinely new worker capability is **MCP-driven conflict resolution** (§5), which today exists only in the UI hub.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Decisions (locked with user, 2026-07-24)
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
|---|---|
|
||||||
|
| Which tasks does the helper handle? | **Free choice, any status.** User hand-picks; helper acts per-status. |
|
||||||
|
| How are tasks selected for a run? | **Checkbox dialog before launch** (candidates listed, user ticks). |
|
||||||
|
| Merge authority / review-gate | **Auto-merge; asks interactively on uncertainty.** The app's per-task diff-gate is intentionally bypassed for helper-driven merges. |
|
||||||
|
| Conflict handling | **Build MCP conflict tools** so the helper resolves conflicts in the working tree itself, asking only when unsure (Option B). The helper must handle **all** cases including parent/children unit merges; where the MCP path doesn't reach, **manual resolution by hand (Edit + git) is an accepted fallback** (user-confirmed 2026-07-24). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UX Flow
|
||||||
|
|
||||||
|
1. **Entry points**
|
||||||
|
- **Per-list:** context-menu item **"Let Claude handle it"** on each user-list row in `ListsIslandView.axaml` (alongside Settings / Worktrees / Open in Explorer).
|
||||||
|
- **Global:** one entry (footer of the lists island) that spans *all* lists/repos.
|
||||||
|
2. Click opens the **Merge Helper selection dialog** (§4): a checkbox list of candidate tasks, grouped by list/repo, pre-filtered to tasks worth acting on but freely overridable.
|
||||||
|
3. User ticks tasks → **"Let Claude handle it"** confirm button.
|
||||||
|
4. UI asks the worker for a `MergeHelperLaunchSpec`, opens a **ConPTY tile in Mission Control** running the real `claude` TUI with the merge-helper prompt.
|
||||||
|
5. The session works through the tasks, printing progress and asking questions inline; the user answers directly in the terminal.
|
||||||
|
6. On completion Claude prints a **summary** (merged / skipped / conflicted / follow-ups). The tile stays open for review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Selection Dialog
|
||||||
|
|
||||||
|
New modal `MergeHelperSelectionDialog` (View + VM), built with the existing `TaskCompletionSource<T>` dialog pattern used by other modals.
|
||||||
|
|
||||||
|
**Contents:**
|
||||||
|
- Title: *"Let Claude handle it"* + subtitle naming the scope ("List: <name>" or "All lists").
|
||||||
|
- A scrollable checkbox list of **candidate tasks**. Per row: checkbox, title, status badge, list/repo name (in global mode).
|
||||||
|
- Grouping: by list/repo in global mode; flat in per-list mode.
|
||||||
|
- Default selection: all **actionable** tasks pre-ticked — actionable = `WaitingForReview`, `Idle`, `Queued`, `Failed` (resettable). `Running` / `WaitingForChildren` shown but unticked (helper will poll them). Terminal `Done`/`Cancelled` excluded from the list entirely.
|
||||||
|
- Footer: **"Let Claude handle it"** (disabled when nothing ticked) + **Cancel**. A "select all / none" affordance.
|
||||||
|
|
||||||
|
**Candidate source:** `list_tasks` via the existing worker client (per list, or across all lists for global). No new query needed; the VM filters client-side by status.
|
||||||
|
|
||||||
|
**Output:** an ordered `IReadOnlyList<string>` of selected task IDs (+ their list/repo mapping), passed to the launch request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. New Worker Capability — MCP Conflict Resolution
|
||||||
|
|
||||||
|
Today (verified): `merge_task` / `review_task approve` call `TaskMergeService.MergeAsync(..., leaveConflictsInTree:false)` — on conflict they run `git merge --abort` (clean rollback, no markers) and return `mergeStatus="conflict"`; the task stays `WaitingForReview`. Continue/abort/write-resolution exist **only** on the SignalR hub (Rider merge editor). An MCP agent therefore cannot resolve conflicts. This section adds that.
|
||||||
|
|
||||||
|
### 5.1 Approach
|
||||||
|
|
||||||
|
Reuse the *exact* engine methods the UI already uses — `TaskMergeService.MergeAsync(leaveConflictsInTree:true)`, `ContinueMergeAsync`, `AbortMergeAsync` — and expose them over MCP. The helper resolves conflict markers on disk (it has filesystem access to the repo checkouts via `--add-dir`, see §6.3) and drives the merge state exclusively through MCP tools so the engine stays authoritative.
|
||||||
|
|
||||||
|
### 5.2 MCP surface changes (`ExternalMcpService.cs`)
|
||||||
|
|
||||||
|
1. **`review_task` / `merge_task` — new optional param `leaveConflictsInTree: bool = false`.**
|
||||||
|
When `true` and the merge conflicts: leave markers in the checkout instead of aborting, and return
|
||||||
|
`{ mergeStatus: "conflict_in_tree", conflicts: string[], repoPath: string }`
|
||||||
|
where `repoPath` is the checkout holding the markers. Task stays `WaitingForReview`, merge is in progress. Clean-merge behaviour is unchanged, so the helper can always pass `true`.
|
||||||
|
|
||||||
|
2. **New tool `continue_merge(taskId)`** → `TaskMergeService.ContinueMergeAsync`.
|
||||||
|
Stages the resolved files and commits the merge; on success the task goes to `Done` and the worktree is marked merged, returning `{ merged: true, mergeCommit }`. If markers remain, returns `{ merged: false, conflicts: string[] }`.
|
||||||
|
|
||||||
|
3. **New tool `abort_merge(taskId)`** → `TaskMergeService.AbortMergeAsync`.
|
||||||
|
Aborts the in-progress merge; task stays `WaitingForReview`. Returns `{ aborted: true }`.
|
||||||
|
|
||||||
|
**Implementation notes (verify against `TaskMergeService.cs` during the plan):**
|
||||||
|
- Confirm the exact signatures of `ContinueMergeAsync` / `AbortMergeAsync` and how in-progress-merge state is keyed. The hub tracks a single active conflict merge; the MCP variants must locate the merge from `taskId` (target branch + repo from the task/list), not shared hub state.
|
||||||
|
- Emit the existing `TaskUpdated` event after continue/abort so the UI list re-buckets live.
|
||||||
|
- Guard against a repo already mid-merge (`Blocked`) — surface it to the agent rather than clobbering.
|
||||||
|
|
||||||
|
### 5.3 Parent/children unit merges
|
||||||
|
|
||||||
|
`review_task approve` on a task **with children** drives `PlanningMergeOrchestrator` (a multi-step unit merge with its own continue/abort on the hub). The helper must handle these too. Two paths, tried in order:
|
||||||
|
|
||||||
|
1. **MCP (preferred):** `continue_merge` / `abort_merge` detect *which* kind of in-progress merge the task has (single-task `TaskMergeService` vs orchestrated `PlanningMergeOrchestrator`) and route to the matching engine continue/abort. This keeps the orchestrated path engine-mediated over MCP too. Implement if the orchestrator's continue/abort can be located from the task without shared hub UI state (verify in A1).
|
||||||
|
2. **Manual fallback (accepted):** where the MCP path genuinely can't reach an in-progress merge, the helper resolves the conflict markers on disk (Read/Edit) and completes the merge by hand (`git add <paths>` + `git commit`, or `git merge --continue`). The user has explicitly accepted hand-merging as a fallback. The system prompt still mandates: **prefer the MCP tools whenever they apply**; only drop to raw git for cases the MCP tools don't cover, and honour the shared-checkout rule (`git commit -- <paths>`, never a bare commit that sweeps peers' index).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Launch — Worker + Wiring
|
||||||
|
|
||||||
|
### 6.1 Prompt templates
|
||||||
|
|
||||||
|
Add `PromptKind.MergeHelper` (system) and `PromptKind.MergeHelperInitial` (brief) to `ClaudeDo.Data/PromptFiles.cs`, with built-in defaults and `{{token}}` rendering, mirroring `Planning` / `PlanningInitial`.
|
||||||
|
|
||||||
|
- **System prompt** (`merge-helper-system.md`): defines the role and the per-status algorithm (§7), the merge/conflict rules, the "ask on uncertainty" posture, and the required final summary format.
|
||||||
|
- **Initial brief** (`merge-helper-initial.md`): rendered with the selected tasks — a table of `{id, title, status, list, repo}` plus the scope label. Written to a session-brief file on disk; the positional prompt is a **single-line kickoff** pointing at that file via `--add-dir` (planning pattern — a multi-line positional prompt truncates at the first newline).
|
||||||
|
|
||||||
|
### 6.2 Session files
|
||||||
|
|
||||||
|
Path: `~/.todo-app/merge-helper-sessions/<sessionId>/` (a fresh GUID per run — these sessions are ephemeral and never resumed):
|
||||||
|
- `brief.md` — rendered task list + scope + instructions.
|
||||||
|
- No per-session MCP config: the session uses the **globally-registered `claudedo` MCP server** (same as task/ad-hoc sessions), so no token is needed.
|
||||||
|
|
||||||
|
Cleanup: prune session dirs older than N days on app start (best-effort; same posture as planning dirs).
|
||||||
|
|
||||||
|
### 6.3 Launch spec
|
||||||
|
|
||||||
|
New `InteractiveLaunchSpecService.BuildForMergeHelper(selectedTaskIds, scope, ct)` returning a `LaunchSpec`:
|
||||||
|
- **Cwd:** per-list → the list's repo working dir; global → the first selected task's repo (any valid repo; the agent works cross-repo via MCP).
|
||||||
|
- **`--add-dir`:** the session-brief dir **plus every distinct repo checkout** among the selected tasks (so the agent can read/resolve conflict markers in each repo). Computed from each task's list working dir.
|
||||||
|
- **Args:** `--permission-mode default`, `--allowedTools mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill`, `--append-system-prompt-file <merge-helper-system.md>`, `--add-dir ...`, then the single-line kickoff prompt.
|
||||||
|
- `Edit` is required for conflict resolution; `Bash` is allowed for **read-only** git inspection (`git status`/`diff`) — the system prompt mandates that all merge *state changes* go through MCP tools, never raw `git merge/commit`, to keep the engine authoritative and honour the user's rejection of the "raw git" option.
|
||||||
|
- **Env:** `MCP_TOOL_TIMEOUT=200000` (as task/ad-hoc sessions set).
|
||||||
|
|
||||||
|
### 6.4 Hub + client + Mission Control
|
||||||
|
|
||||||
|
- **Hub:** `WorkerHub.GetMergeHelperLaunchSpec(string[] taskIds, string? listId)` → `_launchSpecService.BuildForMergeHelper(...)`. Sibling to `GetAdHocLaunchSpec` / `GetPlanningStartLaunchSpec`.
|
||||||
|
- **Client:** `IWorkerClient.GetMergeHelperLaunchSpecAsync(...)` + `WorkerClient` impl.
|
||||||
|
- **Mission Control:** `MissionControlViewModel.OpenMergeHelperConPtySessionAsync(spec)` — wraps the spec in a `TerminalLaunchDescriptor`, creates a `ConPtyPaneViewModel` (ad-hoc style, **never deduped** — each run is its own tile), adds it to `ConPtySessions`.
|
||||||
|
- **Event plumbing:** `ListsIslandViewModel` raises `LetClaudeHandleRequested(scope)`; `IslandsShellViewModel` forwards to Mission Control, which opens the selection dialog, then (on confirm) fetches the spec and opens the tile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Helper Behaviour (encoded in the system prompt)
|
||||||
|
|
||||||
|
For each selected task, act by status:
|
||||||
|
- **Idle / Queued:** `run_task_now`; poll `get_task` until terminal or `WaitingForReview`.
|
||||||
|
- **Failed:** `reset_failed_task` then run, *or* ask the user — failures often need a human call; default to asking briefly.
|
||||||
|
- **Running / WaitingForChildren:** poll `get_task` until it surfaces for review.
|
||||||
|
- **WaitingForReview:** `get_task_diff` (stat first, then full if needed), sanity-check the change against the task's intent, then `review_task approve` with `leaveConflictsInTree:true`.
|
||||||
|
- **Clean →** merged, task Done.
|
||||||
|
- **Conflict (`conflict_in_tree`) →** open the conflicted files under `repoPath` (Read/Edit), resolve the markers guided by both sides' intent, then `continue_merge`. If the resolution is non-obvious or risky, **ask the user in the terminal** before continuing. `abort_merge` if the user declines or it's unsafe.
|
||||||
|
- **Parent with children:** clean unit merge proceeds; on conflict, resolve via the MCP tools if they reach the orchestrated merge, else hand-merge the markers and complete it (§5.3) — asking the user first when the resolution is non-obvious.
|
||||||
|
|
||||||
|
Cross-cutting rules (in the prompt):
|
||||||
|
- Ask the user interactively for anything ambiguous, risky, or destructive — that is the point of the ConPTY session.
|
||||||
|
- Never use raw `git merge/commit/reset`; drive all merge state through the MCP tools.
|
||||||
|
- Keep a running tally; at the end print a **summary**: per task — final status, merge commit (if any), conflicts resolved, anything skipped, and suggested follow-ups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Testing
|
||||||
|
|
||||||
|
**Automated (`ClaudeDo.Worker.Tests`, real SQLite + real git):**
|
||||||
|
- `review_task`/`merge_task` with `leaveConflictsInTree:true`: clean merge → Done; conflicting merge → `conflict_in_tree`, markers present in the checkout, task stays `WaitingForReview`.
|
||||||
|
- `continue_merge`: after markers resolved on disk → commits, task Done, worktree merged; with markers still present → returns remaining conflicts.
|
||||||
|
- `abort_merge`: in-progress merge aborted, markers gone, task stays `WaitingForReview`.
|
||||||
|
- `continue_merge`/`abort_merge` on a task with no in-progress merge → clean MCP error, no clobber.
|
||||||
|
- `TaskUpdated` fired after continue/abort.
|
||||||
|
- `BuildForMergeHelper`: computes distinct repo `--add-dir` set, correct cwd per scope, brief file rendered with all selected tasks, allowed-tools string correct.
|
||||||
|
|
||||||
|
**No real-Claude tests** (per project convention) — the end-to-end ConPTY run is a manual smoke item.
|
||||||
|
|
||||||
|
**Manual (add to `docs/open.md`):**
|
||||||
|
- ConPTY tile launches with the brief; MCP tools reachable; a clean multi-task run merges all and prints a summary.
|
||||||
|
- A seeded conflict is resolved autonomously via `continue_merge`.
|
||||||
|
- Interactive question round-trip (helper asks, user answers in terminal).
|
||||||
|
- Global (multi-repo) run with `--add-dir` for each repo.
|
||||||
|
- Selection dialog: grouping, default ticks, select-all/none, per-list vs global scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Phasing
|
||||||
|
|
||||||
|
Delivered as one plan with three phases (see the plan doc). Phase A is independently useful and merges first.
|
||||||
|
|
||||||
|
- **Phase A — Worker MCP conflict tools** (§5): `leaveConflictsInTree` param + `continue_merge` + `abort_merge` + tests. No UI.
|
||||||
|
- **Phase B — Worker launch** (§6.1–6.4 worker side): prompt templates, `BuildForMergeHelper`, hub endpoint, session-file/brief generation, client method. Contract for C locked here.
|
||||||
|
- **Phase C — UI**: selection dialog (View+VM), per-list + global entries, event plumbing, `OpenMergeHelperConPtySessionAsync`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Out of scope (v1)
|
||||||
|
|
||||||
|
- Resuming a merge-helper session (`--resume`); sessions are ephemeral.
|
||||||
|
- A non-interactive/headless merge-helper (this is deliberately a ConPTY interactive session).
|
||||||
|
- Cross-list *batching* semantics beyond "act on each selected task independently."
|
||||||
|
- Any change to the existing per-task Approve/diff-gate flow.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# "Let Claude handle it" — per-list handler with read / dedupe / enhance / run / merge
|
||||||
|
|
||||||
|
Date: 2026-07-27
|
||||||
|
Supersedes parts of: `2026-07-24-merge-helper-design.md` (global scope, run-then-merge-only prompt)
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
The merge helper shipped in v2.3.0 with two entry points — a per-list context-menu item and a
|
||||||
|
global footer Broom button — and a prompt that only runs and merges the selected tasks.
|
||||||
|
|
||||||
|
Two things are wrong with that:
|
||||||
|
|
||||||
|
- **The global scope is unwanted.** A run spanning several lists spans several repos, which
|
||||||
|
makes `cwd`, the `--add-dir` set and the merge order ambiguous for no benefit. The user
|
||||||
|
works one list (= one repo) at a time.
|
||||||
|
- **The helper starts too late.** It takes the task list as given: it never reads the tasks as
|
||||||
|
a set, so duplicates run twice and produce conflicting worktrees, and vague tasks go into an
|
||||||
|
autonomous run under-specified and come back wrong.
|
||||||
|
|
||||||
|
## 2. Goal
|
||||||
|
|
||||||
|
One entry point, on a user list. It opens an interactive ConPTY session that takes the selected
|
||||||
|
tasks through five phases: read them all, dedupe them, sharpen them for autonomous execution,
|
||||||
|
run them, then review and merge each worktree.
|
||||||
|
|
||||||
|
Non-goals: no change to the autonomous queue path, no change to the ConPTY tile plumbing,
|
||||||
|
no rename of the `MergeHelper*` identifiers (the user-facing label stays "Let Claude handle it").
|
||||||
|
|
||||||
|
## 3. Scope becomes list-only
|
||||||
|
|
||||||
|
`listId` becomes non-nullable across the whole chain:
|
||||||
|
|
||||||
|
| Layer | Change |
|
||||||
|
|---|---|
|
||||||
|
| `ListsIslandViewModel` | `MergeHelperRequest(string ListId, …)`; `LetClaudeHandleAllAsync` deleted |
|
||||||
|
| `IslandsShellViewModel:241` | unchanged (already forwards `req.ListId`) |
|
||||||
|
| `MissionControlViewModel:327` | `OpenMergeHelperConPtySessionAsync(string listId, …)` |
|
||||||
|
| `IWorkerClient:90` / `WorkerClient:525` | `GetMergeHelperLaunchSpecAsync(taskIds, string listId, ct)` |
|
||||||
|
| `WorkerHub:682` | `GetMergeHelperLaunchSpec(string[] taskIds, string listId)` |
|
||||||
|
| `IInteractiveLaunchSpecService:36` | `BuildForMergeHelperAsync(taskIds, string listId, ct)` |
|
||||||
|
|
||||||
|
Deleted UI surface:
|
||||||
|
|
||||||
|
- Broom button `ListsIslandView.axaml:205-210` and `LetClaudeHandleAllCommand`.
|
||||||
|
- `IsGlobal` on `MergeHelperSelectionModalViewModel` and the LIST column
|
||||||
|
(`MergeHelperSelectionModal.axaml:71`) — with a single list the column is constant.
|
||||||
|
- Localization keys `lists.letClaudeAllTip`, `modals.mergeHelper.scopeAll`,
|
||||||
|
`modals.mergeHelper.columnList` (en + de, parity test enforces both).
|
||||||
|
|
||||||
|
`Configure(string listId, string listName)` loses its nullable overload; `ScopeLabel` always
|
||||||
|
renders `modals.mergeHelper.scopeList`.
|
||||||
|
|
||||||
|
### 3.1 Single repo in the launch spec
|
||||||
|
|
||||||
|
`BuildForMergeHelperAsync` currently collects a distinct `repoDirs` set across the selected
|
||||||
|
tasks and picks `cwd` per scope. With a list scope every task shares the list's `WorkingDir`,
|
||||||
|
so this collapses to:
|
||||||
|
|
||||||
|
- Load the list; throw `InvalidOperationException` if it has no existing `WorkingDir`.
|
||||||
|
- `cwd` = that directory; `--add-dir` = the session dir + that one directory.
|
||||||
|
- The per-task brief line drops the now-constant `list:` and `repo:` fields.
|
||||||
|
|
||||||
|
The context-menu item is already hidden when `WorkingDir` is empty, so the throw is a guard,
|
||||||
|
not a normal path.
|
||||||
|
|
||||||
|
### 3.2 Entry point visibility
|
||||||
|
|
||||||
|
The item stays in the list row's context menu, next to "List settings", "Worktrees overview",
|
||||||
|
"Open in Explorer" and "Open in Terminal". That is the established place for list-scoped
|
||||||
|
actions; a second always-visible button in the row would break the pattern.
|
||||||
|
|
||||||
|
## 4. Worker: `Cancelled` becomes externally settable
|
||||||
|
|
||||||
|
`ExternalMcpService.UpdateTaskStatus` accepts only `Idle` and `Queued` and throws
|
||||||
|
`"Status '{target}' is not settable externally. Use run_task_now or cancel_task."` for the
|
||||||
|
rest — but neither escape hatch reaches an **Idle** task: `cancel_task` only cancels a
|
||||||
|
*running* task, and `review_task(decision="cancel")` requires WaitingForReview/Running/Queued.
|
||||||
|
The dedupe phase needs exactly that: retire an Idle duplicate without destroying it.
|
||||||
|
|
||||||
|
Add to the switch:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
case TaskStatus.Cancelled:
|
||||||
|
var cancelResult = await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken);
|
||||||
|
if (!cancelResult.Ok)
|
||||||
|
throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
`TaskStateService.CancelAsync` (`State/TaskStateService.cs:244`) already owns the transition
|
||||||
|
and its worktree/parent side effects. The existing error message in `UpdateTaskStatus` already
|
||||||
|
lists `Cancelled` as valid, so this also removes a lie. A cancelled task stays visible and can
|
||||||
|
be reset to Idle — nothing is lost, unlike `delete_task`.
|
||||||
|
|
||||||
|
## 5. The five-phase prompt
|
||||||
|
|
||||||
|
`PromptFiles.MergeHelperDefault` is rewritten. The session stays interactive and the helper is
|
||||||
|
told to ask whenever unsure — that is the point of a watched ConPTY session.
|
||||||
|
|
||||||
|
### Phase 0 — Read
|
||||||
|
|
||||||
|
`batch_get_tasks` over every id in the brief before touching anything: title, description,
|
||||||
|
status, parent/child links. The helper must hold the whole set in mind before acting on any
|
||||||
|
single task.
|
||||||
|
|
||||||
|
### Phase 1 — Dedupe
|
||||||
|
|
||||||
|
Compare the tasks pairwise for overlap. Emit a table of candidate pairs with the reason each
|
||||||
|
pair looks like a duplicate, then **ask per pair**:
|
||||||
|
|
||||||
|
- merge → fold the loser's unique content into the survivor via `update_task`, then
|
||||||
|
`update_task_status(loserId, "Cancelled")`;
|
||||||
|
- keep both → note why and move on.
|
||||||
|
|
||||||
|
Nothing is cancelled without an explicit answer.
|
||||||
|
|
||||||
|
### Phase 2 — Enhance
|
||||||
|
|
||||||
|
For each surviving task, sharpen title and description for autonomous execution:
|
||||||
|
|
||||||
|
- concrete acceptance criteria,
|
||||||
|
- the files/areas actually involved — grounded in the repo via Read/Grep/Glob, not guessed,
|
||||||
|
- explicit out-of-scope.
|
||||||
|
|
||||||
|
Write back with `update_task` (title / description / commitType are the settable fields; it
|
||||||
|
refuses while Running, which cannot happen this early). Rules: do not change intent, do not
|
||||||
|
invent requirements. A task too vague to sharpen safely gets a question, not a guess.
|
||||||
|
|
||||||
|
### Phase 3 — Run
|
||||||
|
|
||||||
|
`run_task_now` cannot be used for a batch: `OverrideSlotService.StartInSlot`
|
||||||
|
(`Queue/OverrideSlotService.cs:65-70`) holds a single slot and throws `"override slot busy"`
|
||||||
|
on the second concurrent call. The queue picker is the only parallel path.
|
||||||
|
|
||||||
|
So: read `get_app_settings`, tell the user how many parallel slots are configured
|
||||||
|
(`MaxParallelExecutions`, default 1 — `AppSettingsEntity.cs:14`), then
|
||||||
|
`update_task_status(id, "Queued")` for every surviving task, then poll `get_task` until each
|
||||||
|
has left Queued/Running — `WaitingForReview` on success, `Failed` on error. Announcing the
|
||||||
|
slot count up front stops the user wondering why "run them all" executes one at a time.
|
||||||
|
|
||||||
|
A task already `Running` or `WaitingForChildren` when the session starts is not re-queued, only
|
||||||
|
polled. A task already `WaitingForReview` skips straight to Phase 4.
|
||||||
|
|
||||||
|
### Phase 4 — Review and merge
|
||||||
|
|
||||||
|
Sequential, in list order. A task that came back `Failed` needs human judgement — ask whether
|
||||||
|
to `reset_failed_task` and re-queue it, or skip it. Otherwise, unchanged from the shipped
|
||||||
|
prompt: `get_task_diff` (stat first,
|
||||||
|
full diff when non-trivial), sanity-check against the task's intent, ask before merging
|
||||||
|
anything that looks wrong, then `review_task(taskId, decision="approve",
|
||||||
|
leaveConflictsInTree=true)` and the conflict loop (`continue_merge` / `abort_merge`, parent id
|
||||||
|
for unit merges, hand-resolution only where MCP cannot reach, always
|
||||||
|
`git commit -- <paths>` and never `git add -A` because the checkout is shared).
|
||||||
|
|
||||||
|
New in this phase: the prompt states that because Phase 3 branches all fork from the same
|
||||||
|
base, **conflicts are the normal case, not an exception** — resolve them rather than bailing
|
||||||
|
out of the run.
|
||||||
|
|
||||||
|
### Phase 5 — Summary
|
||||||
|
|
||||||
|
One line per task: title — dedupe action — enhanced? — final status — merge commit —
|
||||||
|
conflicts resolved. Then anything skipped and why, then follow-ups.
|
||||||
|
|
||||||
|
### Brief template
|
||||||
|
|
||||||
|
`MergeHelperInitialDefault` names the list and repo once in the header and drops the per-task
|
||||||
|
`list:`/`repo:` fields, leaving `- [{status}] {title} (id: {id})`. Descriptions stay out of the
|
||||||
|
brief; Phase 0 fetches them.
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
| Test | Change |
|
||||||
|
|---|---|
|
||||||
|
| `MergeHelperSelectionModalViewModelTests` | drop `IsGlobal`, list-scoped `Configure` |
|
||||||
|
| `InteractiveLaunchSpecServiceTests` | three `null`-listId cases → non-null; drop the two-repo global-scope test; add "list without WorkingDir throws" |
|
||||||
|
| `MissionControlViewModelTests` | four `OpenMergeHelperConPtySessionAsync(null, …)` calls |
|
||||||
|
| `StubWorkerClient`, `TasksIslandViewModelPlanningTests` fake | signature |
|
||||||
|
| `PromptFilesTests` | assert the five phase markers in the default prompt |
|
||||||
|
| `Localization.Tests` | parity after removing three keys |
|
||||||
|
| new: `ExternalMcpService` / worker test | `UpdateTaskStatus(id, "Cancelled")` cancels an Idle task; unknown status still throws |
|
||||||
|
|
||||||
|
No test spawns the real `claude` CLI — the prompt content is asserted as text, the session
|
||||||
|
itself is a manual smoke step.
|
||||||
|
|
||||||
|
## 7. Verification left to the user
|
||||||
|
|
||||||
|
- The list context menu shows "Let Claude handle it" only for lists with a working dir, and
|
||||||
|
the Broom button is gone from the footer row.
|
||||||
|
- The selection dialog has no LIST column and reads "List: <name>".
|
||||||
|
- A real ConPTY run: dedupe questions appear, enhancements land in the task descriptions,
|
||||||
|
queued tasks execute, merges complete or hand off to conflict resolution.
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Usage-Optimierung — Befunde und Messmethodik
|
||||||
|
|
||||||
|
Stand: 2026-08-05. Ausgangsfrage: Wie lassen sich autonome ClaudeDo-Agents
|
||||||
|
token-effizient betreiben? Abrechnung läuft über **Abo-Session-Limits, nicht über
|
||||||
|
die API** — relevant ist roher Token-Verbrauch gegen 5h-/7d-Fenster, nicht Geld.
|
||||||
|
|
||||||
|
Dieses Dokument ist bewusst kompakt gehalten. Was eine Session früh in den Prefix
|
||||||
|
lädt, wird mit jeder weiteren Nachricht multipliziert (siehe Befund 5) — ein
|
||||||
|
30k-Handover-Dokument würde das Problem reproduzieren, das es beschreibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Verbrauchsstruktur
|
||||||
|
|
||||||
|
Gemessen über alle Transcripts unter `C:\Users\mika.kuns\.claude\projects`.
|
||||||
|
|
||||||
|
| Posten | Roh-Token | Anteil |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Cache-Read | 3.118.025.644 | 95,6 % |
|
||||||
|
| Cache-Write | 140.783.227 | 4,3 % |
|
||||||
|
| Output | 22.466.702 | 0,7 % |
|
||||||
|
| Input frisch | 141.496 | 0,0 % |
|
||||||
|
|
||||||
|
**Kontext-Resend = 99,3 % des Rohverbrauchs.** Auch unter API-Preisgewichtung
|
||||||
|
(read 0,1× / write 1,25× / out 5×) bleibt es bei 81,3 %. Die Rangfolge ist gegen
|
||||||
|
jede plausible Gewichtung robust.
|
||||||
|
|
||||||
|
Verstärkung: ~3,6 Mio einzigartiger Inhalt → 1,59 Mrd abgerechnete Prompt-Token
|
||||||
|
in ClaudeDo-Sessions. **Jeder Kontext-Token wird im Schnitt ~425× erneut
|
||||||
|
abgerechnet.**
|
||||||
|
|
||||||
|
## 2. Scope-Split — wer verbraucht
|
||||||
|
|
||||||
|
| Scope | Roh-Token | Anteil |
|
||||||
|
|---|---:|---:|
|
||||||
|
| INTERAKTIV (Mikas eigene Sessions) | 2.690.028.785 | **81,6 %** |
|
||||||
|
| AGENT (ClaudeDo-Runs) | 606.222.055 | **18,4 %** |
|
||||||
|
|
||||||
|
Interaktiv nach Modell: opus-4-8 33,0 % · opus-5 28,5 % · sonnet-5 14,0 % ·
|
||||||
|
fable-5 5,2 %. Agent-Runs fahren überwiegend sonnet-5 — der Default greift, die
|
||||||
|
Modelldisziplin auf Agent-Seite ist in Ordnung.
|
||||||
|
|
||||||
|
**Konsequenz:** ClaudeDo-Optimierung adressiert maximal 18 % des Limits. Der
|
||||||
|
größere Block sind die eigenen Opus-Sessions.
|
||||||
|
|
||||||
|
## 3. Session-Länge ist der Treiber
|
||||||
|
|
||||||
|
Verbrauch ≈ Nachrichten × Ø-Kontext, und der Kontext wächst mit den Nachrichten →
|
||||||
|
**quadratisch**. Eine Session in k Teile schneiden bringt grob 1/k.
|
||||||
|
|
||||||
|
Interaktiv: **Top 20 von 255 Sessions = 55,9 % des Verbrauchs.**
|
||||||
|
|
||||||
|
| Roh-Token | Msgs | Ø Kontext | Modell |
|
||||||
|
|---:|---:|---:|---|
|
||||||
|
| 199.516.097 | 716 | 278.653 | opus-4-8 |
|
||||||
|
| 138.020.247 | 440 | 313.682 | opus-5 |
|
||||||
|
| 123.586.522 | 463 | 266.925 | opus-4-8 |
|
||||||
|
|
||||||
|
## 4. Was den Kontext füllt
|
||||||
|
|
||||||
|
| Tool | Anteil (Agent) | Anteil (interaktiv) |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Read | 59,8 % | 68,2 % |
|
||||||
|
| Bash | 16,3 % | 15,4 % |
|
||||||
|
| Grep | 8,3 % | 5,2 % |
|
||||||
|
|
||||||
|
Read ohne `offset`/`limit`: **57 % (Agent) / 62 % (interaktiv)**.
|
||||||
|
Re-Reads derselben Datei in derselben Session: 18 % der Read-Calls.
|
||||||
|
Subagent-Nutzung: nur 40 Calls bei 1.325 Reads (Agent-Seite) — die
|
||||||
|
Kontext-Firewall ist praktisch ungenutzt. Interaktiv sind Subagent-Sessions
|
||||||
|
dagegen 20 % des Verbrauchs.
|
||||||
|
|
||||||
|
Teuerste Read-Ziele sind God-Files: `TasksIslandViewModel.cs` (1121 Z.),
|
||||||
|
`ExternalMcpService.cs` (1002 Z.), `WorkerHub.cs` (979 Z.).
|
||||||
|
|
||||||
|
## 5. Prefix-Größe × Nachrichtenzahl schlägt alles
|
||||||
|
|
||||||
|
Fallstudie an der Analyse-Session selbst (136 Msgs, 43.492.835 Roh-Token):
|
||||||
|
|
||||||
|
```
|
||||||
|
msg 1 : 39.914
|
||||||
|
msg 3 : 273.822 (+233.908) <-- claude-api-Skill geladen
|
||||||
|
msg 136: 380.327
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Skill in Nachricht 3 wurde danach 136× mitgelesen: **31,8 Mio Token = 73 %
|
||||||
|
der Session**. Zum Vergleich: **alle tool_results zusammen = 21.600 Token
|
||||||
|
(0,05 %)**.
|
||||||
|
|
||||||
|
Discovery ist praktisch gratis, wenn man aggregiert statt Dateien dumpt. Teuer ist
|
||||||
|
ausschließlich, was früh und groß in den Prefix wandert. Derselbe Block in
|
||||||
|
Nachricht 120 geladen hätte ein Zwanzigstel gekostet.
|
||||||
|
|
||||||
|
## 6. Amortisation von Planungssessions
|
||||||
|
|
||||||
|
| Posten | Token |
|
||||||
|
|---|---:|
|
||||||
|
| Analyse-Session gesamt | 43,5 Mio |
|
||||||
|
| davon vermeidbarer Prefix-Ballast | −31,8 Mio |
|
||||||
|
| echte Planungsleistung | ≈ 11,7 Mio |
|
||||||
|
|
||||||
|
Gegenrechnung: **10 von 37 Tasks brauchten einen Retry (27 %)**, 13 von 54 Runs
|
||||||
|
sind Wiederholungen. Ein Agent-Run liegt im Schnitt bei ~11 Mio Roh-Token — ein
|
||||||
|
vermiedener Retry spart also grob eine ganze Planungssession. Dazu ersparte
|
||||||
|
Rediscovery: ein Fund von ~8k Token, den ein Agent in Turn 10 eines 60-Turn-Runs
|
||||||
|
selbst machen müsste, wird danach ~50× mitgelesen = ~400k pro Fund.
|
||||||
|
|
||||||
|
**Fazit: gründliche Planung rechnet sich — aber nur bei schlankem Prefix.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was NICHT hilft (geprüft und verworfen)
|
||||||
|
|
||||||
|
- **`--effort` senken.** Thinking ist 12.536 Token über alle interaktiven
|
||||||
|
Sessions = **0,00 %** des Verbrauchs. Effort zu senken spart nichts und kostet
|
||||||
|
Qualität. Endgültig erledigt.
|
||||||
|
- **Skill-Trigger entschärfen.** Der `claude-api`-Skill kostete in einer Session
|
||||||
|
73 % — feuerte aber nur **2× in 3 Wochen** (2026-07-14, 2026-08-05). Erwarteter
|
||||||
|
Nutzen vernachlässigbar, Risiko (Antworten aus veraltetem Prior) real. Er hat
|
||||||
|
sich in dieser Session sogar bezahlt gemacht: der Hinweis, dass `input_tokens`
|
||||||
|
nur der uncached Rest ist, hat den Accounting-Bug aufgedeckt.
|
||||||
|
- **God-Files splitten.** Eigenes Risiko; Read-Disziplin entschärft das Symptom
|
||||||
|
billiger.
|
||||||
|
|
||||||
|
## Offene Hebel — noch nicht untersucht
|
||||||
|
|
||||||
|
- Interaktive Session-Hygiene: ab wann lohnt `/clear`, messbar an Ø-Kontext?
|
||||||
|
- Modellrouting interaktiv (opus-4-8 + opus-5 = 61,5 % des Accounts).
|
||||||
|
- Subagent-Ökonomie: Firewall-Nutzen gegen Eigenverbrauch (20 % interaktiv).
|
||||||
|
- MCP-Tool-Definitionen im Prefix: Umfang bei ~200 deferred Tools nicht gemessen.
|
||||||
|
- Kalibrierung der tatsächlichen Limit-Gewichtung gegen die Rohtoken-Zahlen
|
||||||
|
(braucht `/usage`-Ausgabe; der OAuth-Endpoint ist für Claude nicht zugänglich,
|
||||||
|
weil `~/.claude/.credentials.json` hart blockiert ist).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Messmethodik (reproduzierbar)
|
||||||
|
|
||||||
|
Datenquelle: `C:\Users\mika.kuns\.claude\projects\**\*.jsonl`, ein Record je
|
||||||
|
Message, Verbrauch in `message.usage`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
raw = (u.get('input_tokens',0) + u.get('output_tokens',0)
|
||||||
|
+ u.get('cache_read_input_tokens',0) + u.get('cache_creation_input_tokens',0))
|
||||||
|
```
|
||||||
|
|
||||||
|
Kontextgröße einer Message = `input_tokens + cache_read + cache_write` (ohne
|
||||||
|
Output). Der Verlauf über die Session zeigt Prefix-Sprünge.
|
||||||
|
|
||||||
|
### Fallstricke
|
||||||
|
|
||||||
|
1. **Scope-Filter.** NICHT nach Pfadname `claudedo` filtern — das zieht
|
||||||
|
interaktive Sessions im ClaudeDo-Repo mit rein und verfälscht den Split massiv
|
||||||
|
(53 % statt korrekt 18,4 %). Agent-Runs erkennt man an `claudedo-worktrees`
|
||||||
|
oder `sandbox` im Projektordner, so wie es `TranscriptUsageReader` macht.
|
||||||
|
2. **`<synthetic>`-Messages** überspringen — keine echten API-Calls.
|
||||||
|
3. **`task_runs.tokens_in` ist unbrauchbar** — liest nur `input_tokens`, also den
|
||||||
|
uncached Rest. Lag bei einer 79-Mio-Token-Session bei 200 (Faktor ~400.000).
|
||||||
|
`tokens_out` zusätzlich 3,8× zu niedrig. Siehe Task `38394081`.
|
||||||
|
4. **In Bash absolute Pfade verwenden** — `$HOME`/`~` zeigt auf dieser Maschine
|
||||||
|
auf das P:-Laufwerk, nicht auf `C:\Users\mika.kuns`.
|
||||||
|
5. **DB nie direkt lesen** während die App läuft — Kopie ziehen (`todo.db` +
|
||||||
|
`todo.db-wal`).
|
||||||
|
6. **Grundrate prüfen, bevor ein Hebel empfohlen wird.** Ein teurer Einzelfall ist
|
||||||
|
kein Muster (siehe Skill-Trigger oben).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abgeleitete Tasks (Liste „Claude do")
|
||||||
|
|
||||||
|
Empfohlene Reihenfolge:
|
||||||
|
|
||||||
|
| # | ID | Titel |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `1b599d67` | Prompt-Dateien frieren Default ein — `SuggestImprovement`/`AskUser` tot |
|
||||||
|
| 2 | `38394081` | Limit-Verbrauch pro Run sichtbar machen (Cache-Token in `task_runs`) |
|
||||||
|
| 3 | `0d0aa8b0` | Read-Disziplin + Explorer-Subagent im System-Prompt |
|
||||||
|
| 4 | `2de2f008` | `max_turns` deckeln (Ceiling, Defaults, UI-Warnung) |
|
||||||
|
| 5 | `87105f5e` | UsageGate: Parallelität stufenweise drosseln |
|
||||||
|
|
||||||
|
(1) zuerst, weil es ein echter Funktionsbug ist und Voraussetzung dafür, dass (3)
|
||||||
|
den Nutzer überhaupt erreicht. (2) als Nächstes, weil ohne korrektes Accounting
|
||||||
|
nichts messbar ist.
|
||||||
|
|
||||||
|
### Ist-Zustand zum Zeitpunkt der Analyse
|
||||||
|
|
||||||
|
- `app_settings`: `default_model` = sonnet, `default_max_turns` = 100,
|
||||||
|
`max_parallel_executions` = 3, `model_presets` = **null** (fällt auf Defaults
|
||||||
|
zurück: haiku 20 / sonnet 30 / opus 40 / fable 25 — greifen faktisch nie).
|
||||||
|
- 15 Tasks überschreiben `max_turns` nach oben: 10× auf 100, 5× auf 200.
|
||||||
|
- UsageGate existiert bereits: 5h @ 80 %, 7d @ 90 %
|
||||||
|
(Migration `20260805074906_AddUsageGateAndRunModel`, dieselbe Migration hat auch
|
||||||
|
die `model`-Spalte auf `task_runs` gebracht).
|
||||||
|
- `~/.todo-app/prompts/system.md` stammt vom 2026-06-04 und weicht vom
|
||||||
|
`SystemDefault` in `PromptFiles.cs` ab; `agent.md` und `planning.md` dort sind
|
||||||
|
verwaiste Reste eines alten Namensschemas.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Verifikations-Handoff (Stand 2026-07-24)
|
||||||
|
|
||||||
|
Manuelle Verifikation am laufenden System. **Mika bedient die UI, die Session protokolliert Pass/Fail** und bereitet Fixtures vor (Tasks via ClaudeDo-MCP, Git-Setups im Testrepo). Aktive Findings landen in `docs/open.md`; hier steht der Fortschritt je Abschnitt.
|
||||||
|
|
||||||
|
## Handoff für die nächste Session
|
||||||
|
|
||||||
|
**Erledigt (2026-07-24):** §1 Detail-Insel/Diff-Viewer (bis auf DiffModal-Fehler-State), §2 Worktree-Pipeline (alle 3), §3 Planning-Walkthrough (inkl. UnfinishedPlanning-Modal: dismiss/Finalize/Discard PASS, **Resume BUG**), §4 Merge-Editor (Single-Task **und** Planning-Unit-Konflikt **und** Abort), §6 Pick-up-Gating (Code), §7 AskUser (Happy-Path + Timeout + UI-Cleanup; Finding: Banner nur in Mission Control), §8 Session Skills (Install/Aktivierung/Seeding/Gegenprobe/Remove; Findings: fehlender Empty-State + abweichendes Agent-Gear-Icon), §9 Attachments (Drag&Drop-UI + MCP + ComposedPreview; Finding: intermittenter erster-Drop-Fehler), §12 RunNow (moot).
|
||||||
|
|
||||||
|
**Noch offen:**
|
||||||
|
- **§10 Daily Prep/Weekly** — **Bewusst zurückgestellt (2026-07-24)**: Mika will Daily Prep + Weekly Report ohnehin überarbeiten — Verifikation lohnt erst nach dem Rework.
|
||||||
|
- **§11 Self-Update/Autostart** — **Nicht formell gefahren, aber laut Mika „läuft bisher sehr gut" (2026-07-24)** → als OK behandelt; bei Bedarf später gezielt nachtesten.
|
||||||
|
- ~~**Kanten:** §1 DiffModal-Fehler-State, §3 UnfinishedPlanning-Modal, §4 Abort~~ — **alle erledigt (2026-07-24)**: §4 Abort PASS, §3 Modal (Finalize/Discard PASS, Resume BUG), §1 via Code-Analyse geklärt (defensiv/unerreichbar).
|
||||||
|
|
||||||
|
**Vorbedingungen:** Worker + App laufen (SignalR 37821, External MCP 47822 — lt. `~/.todo-app/worker.config.json`). Testliste **`ClaudeDoTests`** (`C:\TestRepos\ClaudeDoTests`, listId `e7992fee6035448394b646d069690e05`) für zerstörungsfreie Runs.
|
||||||
|
|
||||||
|
**Wichtige Gotchas / Lessons (diese Session):**
|
||||||
|
- **NIE `model:"haiku"` für schreibende Task-/Verifikations-Runs** — unter dem Default `--permission-mode auto` werden haiku-Writes denied (still no-op). Immer **sonnet**. (Memory `auto_permission_haiku_footgun`.)
|
||||||
|
- **Merge-Preflight blockt bei dirty Ziel-Working-Tree** (getrackte uncommittete Änderung im `main`-Checkout) — und **Approve schluckt das still** (Bug, open.md). Vor Merge-Checks `git -C C:\TestRepos\ClaudeDoTests status --short` prüfen; fremde WIP nur mit Rücksprache verwerfen (Memory `shared_worktree_partial_commits`: nur pfad-scoped committen).
|
||||||
|
- **Konflikt-Fixture-Rezept:** Task (sonnet) überschreibt eine getrackte Datei im Branch → danach dieselbe Datei auf `main` divergent ändern + `git commit -- <pfad>` → Approve konfliktet. Für Planning-Unit-Konflikt: main-Edit erst scharfschalten, wenn der Parent in WaitingForReview steht.
|
||||||
|
- **Datenwahrheit** notfalls direkt aus der DB: `sqlite3 -readonly ~/.todo-app/todo.db "SELECT substr(id,1,8),status,planning_phase,substr(blocked_by_task_id,1,8),title FROM tasks WHERE …"`.
|
||||||
|
|
||||||
|
**Testrepo-/Fixture-Zustand:** Aufgeräumt (2026-07-24, §7+§8-Session-Ende) — alle `verif §7/§8`-Tasks gelöscht, deren Worktrees + Branches entfernt, `ClaudeDoTests`-`main` unangetastet bei `ecad650` (Tree clean; die §7-Runs liefen isoliert in Worktrees, main nie berührt). ponytail-Skills nach dem Remove-Test wieder deinstalliert (`session_skills` leer, `~/.todo-app/session-skills/` leer). Keine `verif`-Tasks und keine claudedotests-Worktrees übrig. Die übrigen `claudedo/*`-Branches im Testrepo stammen aus früheren Sessions — nicht anfassen. **Die nächste Session baut Fixtures frisch** (Rezepte s. Gotchas oben).
|
||||||
|
|
||||||
|
**Erfasste Folge-Tasks** (Liste „Claude do", Idle, brauchen Brainstorm vor Umsetzung): `f9809a93` Approve erzwingt Diff/Review vor Merge (+ blocked-Merge-Silent-Fail-Fix), `5d627df8` Planning-Session über embedded ConPTY statt wt (+ Planning-Permission-Prompt).
|
||||||
|
|
||||||
|
**Fix-Session:** Alle fixbaren Findings sind für eine frische Session in **`docs/fix-plan-2026-07-24.md`** aufbereitet (gruppiert nach Fixbarkeit: A mechanisch, B error-surfacing, C entscheidungsbedürftig, D investigation, E nits). Volltext je Finding bleibt in `docs/open.md`.
|
||||||
|
|
||||||
|
**Nacharbeit:** Erledigtes aus `docs/open.md` austragen; dieses File löschen, sobald §10 (nach Rework) + evtl. §11-Nachtest erledigt sind.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Detail-Insel & Diff-Viewer (reines Durchklicken)
|
||||||
|
|
||||||
|
- [~] Detail-Insel komplett: Output/Git/Session-Tabs, Merge-Sektion, Agent-Settings-Overrides (InheritedBadge korrekt), Prep-Panel — nach dem VM-Split (`DetailsIslandViewModel` → Sektions-VMs) alles gebunden, keine leeren Panels. — **TEILWEISE (2026-07-24, User-Sichtprüfung an `verif §1 diff matrix`):** Output/Git-Tabs, Merge-Sektion, Agent-Badges vorhanden & gebunden, keine leeren Panels. Git-Tab: `+1 −37`, „merges cleanly" korrekt. **BUG:** OUTCOME-Karte zeigt rohes JSON (s. open.md). Session-Tab fehlt — **erwartet** (nur bei Parent mit Kindern, `HasChildOutcomes`; wird in §4/§3 mit echtem Parent geprüft). Minor: TurnsText `0/max` bei terminalem Reload (open.md). Prep-Panel → §10.
|
||||||
|
- [~] Diff-Viewer: Dateiliste, Added/Deleted/Renamed/Binary-Erkennung, Commit-Range-Diff nach einem Merge. — **Added/Deleted/Binary PASS**; **Renamed** erkannt + „R"-Badge, aber schwach dargestellt (kein alt→neu-Pfad, „+0 −0" — nit, s. open.md); „Review Combined Diff" korrekt ausgegraut (kein Planning-Parent). **Commit-Range-Diff nach Merge: PASS (2026-07-24)** — Diff des gemergten Done-Tasks rendert über `base..head` trotz entferntem Worktree.
|
||||||
|
- [x] DiffModal-Fehler-State: Commit-Range ohne aufgezeichnete Commits → „Diff nicht mehr verfügbar" statt Crash/leer. — **GEKLÄRT via Code-Analyse (2026-07-24): der Fehler-State ist defensiv/unerreichbar.** `vm.diff.unavailable` (DiffViewerViewModel.cs:117-119) feuert nur bei `FromCommitRange && (BaseRef==null || HeadCommit==null)` oder `WorktreePath==null`. Alle Aufrufer sind gegated: `MergeSectionViewModel.OpenDiffAsync` ruft `ConfigureCommitRange` nur unter `CanDiffMergedRange` (base **und** head non-null) und `ConfigureWorktree` nur unter `hasLiveWorktree` (Pfad non-null + existiert); `WorktreesOverviewModalViewModel` übergibt `row.Path` (non-null). Damit können die Null-Checks über die UI nie wahr werden — der „Crash/leer"-Fall, den der Check absichern wollte, ist durch die CanExecute-/Modus-Gates ausgeschlossen. (Fehlende Commit-Objekte bei vorhandenem base+head → `GetCommitRangeDiffAsync` wirft → „loadFailed", nicht „unavailable".) Nicht manuell auslösbar; kein Bug.
|
||||||
|
- [x] „children need attention"-Band auf dem Session-Tab eines Parents mit failed/blocked Kind. — **PASS (2026-07-24, in §3)**: Band + OUTCOMES-Liste mit Roadblock-Hinweis auf dem Session-Tab des Planning-Parents (`verif §3`, Roadblock-Kind).
|
||||||
|
|
||||||
|
## 2. Worktree-Pipeline (3 falsifizierbare Fälle)
|
||||||
|
|
||||||
|
- [x] Happy-Path: Task mit WorkingDir → `worktrees.state='active'`, `head_commit` gesetzt, `diff_stat` non-empty, Branch `claudedo/<id[:8]>` existiert auf Disk. — **PASS (2026-07-24)** via Task `e63eb2f5` (sonnet): Worktree active, `headCommit 8a3ae86` (ahead=1 vom Base), `diff_stat` non-empty (`review-cancel.txt`), Branch `claudedo/e63eb2f5…` auf Disk. (Mein erster Versuch mit `model=haiku` schlug fehl — das war ein haiku/auto-Footgun, keine Pipeline-Sache; s. open.md.)
|
||||||
|
- [x] No-Changes-Run: → `status='Done'`, `head_commit IS NULL`, `diff_stat IS NULL`. — **PASS (2026-07-24)**; Präzisierung: aktueller Flow endet in `WaitingForReview` (nicht `Done`, das ist erst nach Approve) — Doc-„Done" ist veraltet. Kein neuer Commit, leerer Diff bestätigt.
|
||||||
|
- [x] Kein Git-Repo (WorkingDir = `C:\Temp`): → `status='Failed'`, KEINE `worktrees`-Row, Git-Fehler im Log. — **PASS (2026-07-24)**; Fehler: „Worktree creation failed: working_dir is not a git repository".
|
||||||
|
|
||||||
|
## 3. Planning-Flow-Walkthrough
|
||||||
|
|
||||||
|
- [x] Draft → Finalize → Kette: Finalize queued NICHT automatisch (Kinder bleiben Idle); „Queue plan" setzt alle nicht-terminalen Kinder Queued, Kette läuft sequenziell durch (blocked-by löst sich je Vorgänger). — **PASS (2026-07-24, End-to-End mit `verif §3`)**: 3 Kinder gedraftet, Finalize → Parent WaitingForChildren+finalized, Kinder Idle+Kette (clean→conflict→roadblock), Queue plan → sequenzieller Durchlauf, alle Done. **Begleit-Findings (open.md):** „Waiting for Improvements"-Mislabel, Kind-Badge-Live-Refresh (Draft→Planned erst nach Listenwechsel), Kette nicht visualisiert, planning-aktiver Parent zeigt „Idle", Dequeue-X fehlt auf wartenden Kindern, Planning-Permission-Prompt, Planning nutzt wt statt ConPTY.
|
||||||
|
- [x] Parent landet nach letztem terminalen Kind in WaitingForReview; Approve merged die ganze Unit (Parent-Worktree falls Active + jedes Done-Kind in Reihenfolge). — **PASS (2026-07-24)**: Parent → WaitingForReview auch mit Roadblock-Kind; Approve → Unit-Merge (child-clean clean, child-conflict → Konflikt-Editor pro Subtask → aufgelöst → Continue, child-roadblock no-op) → Parent Done, Merge gelandet.
|
||||||
|
- [x] UnfinishedPlanning-Modal: Resume / FinalizeNow / Discard. — **GETESTET (2026-07-24, `verif §3 unfinished-planning` + `verif §3 resume-discard`)**: Modal wird via Rechtsklick→„Resume planning session" ausgelöst, zeigt Titel + „N draft task(s) waiting to be finalized" + Buttons Discard/Finalize/Resume + ×. **× (dismiss)**: No-op (State bleibt `active`). **Finalize**: Parent → `finalized`+`WaitingForChildren`, Kinder Idle+Blocked-by-Kette (= Planned) — PASS. **Discard**: Draft-Kinder gelöscht, Parent → `none`+`idle` — funktional PASS. **Resume: BUG — macht nichts** (`planning_session_id` nie erfasst → `ResumeAsync` wirft „No Claude session ID captured yet", vom UI im leeren `catch` verschluckt; s. open.md). Beide State-ändernden Aktionen reproduzieren das Kind-Rows-Live-Refresh-Finding (Finalize: Badges stale; Discard: gelöschte Rows bleiben bis Reload; open.md).
|
||||||
|
|
||||||
|
## 4. Merge-Editor (Rider-Style 3-Pane) mit echtem Konflikt
|
||||||
|
|
||||||
|
Konflikt provozieren: gleiche Datei auf main ändern, während der Task-Branch sie ändert. Beide Wege testen: **(a)** Single-Task-Approve mit Konflikt, **(b)** Planning-Unit-Merge mit Konflikt in einem Subtask (`PlanningMergeConflict` → Editor öffnet pro Subtask).
|
||||||
|
|
||||||
|
**Verifiziert 2026-07-24 (Single-Task-Approve mit Konflikt, 2 Dateien, `verif §4`): End-to-End PASS** — Approve→Konflikt→3-Pane-Resolver→beide Dateien auflösen→Continue→Merge→Task Done (Merge-Commit gelandet). Sauberer additiver Approve (`verif §1b`, kein Konflikt) → direkt gemergt → Done, kein Editor: **PASS (2026-07-24)**. ⚠ **Vorbedingung-Fund:** ein dirty Ziel-Working-Tree (getrackte uncommittete Änderung) blockt den Merge und Approve schluckt das **still** (s. open.md „Approve & Merge schluckt blocked"). Planning-Unit-Merge-Konflikt (Weg b): **PASS (2026-07-24, in §3)** — Approve eines Planning-Parents mit einem konfliktenden Subtask öffnet den 3-Pane-Editor pro Subtask (`PlanningMergeConflict`), Continue führt den Unit-Merge fort → Parent Done.
|
||||||
|
|
||||||
|
- [x] Drei Panes: MAIN read-only | Result editierbar | INCOMING read-only; Konfliktblöcke rot, aufgelöst grün, in allen Panes. — **PASS**.
|
||||||
|
- [x] Gutter-Toggle `›`/`‹`: Seite rein/raus, Klickreihenfolge = Reihenfolge im Result; main/incoming/beide/keine möglich. — **PASS**.
|
||||||
|
- [x] Nur Konfliktregionen im Result editierbar (Stable read-only); Edits fließen in den Block zurück. — **PASS**.
|
||||||
|
- [~] Synchrones vertikales Scrollen; File-Switcher bei mehreren Dateien; `M conflicts · K resolved`-Readout; Conflict-Ruler (Klick springt). — **PASS**, aber **Multi-File schlecht erkennbar** (2 Konfliktdateien schwer zu sehen — UX-Nit, open.md).
|
||||||
|
- [~] Continue erst aktiv, wenn ALLE Konflikte in ALLEN Dateien gelöst; Binär-Guard greift. — **funktional PASS** (merged erst nach Auflösung beider Dateien), aber **Button ist klickbar statt disabled** solange offen → stummer No-op (UX-Bug, open.md). Binär-Guard hier n/a.
|
||||||
|
- [x] Abort: Tree sauber, Task bleibt WaitingForReview. — **PASS (2026-07-24, `verif §4 abort`)**: Single-Task-Approve mit divergentem main→Konflikt→3-Pane-Editor→**Abort**: Editor schließt, Task bleibt `WaitingForReview`, `main`-Tree sauber (kein `MERGE_HEAD`, kein dirty), `main`-Inhalt + HEAD unverändert.
|
||||||
|
- [FEATURE-WUNSCH] farbliches Hervorheben der übernommenen/eingefügten Zeilen im Result-Pane (open.md).
|
||||||
|
- Bekannte Kanten (nur gegenprüfen, nicht als Fail werten): leere Ours-Seite → null-lange Result-Region (Accept geht, Handtippen fummelig); Gutter-Y-Ausrichtung bei sehr hohen Fenstern/großem Scroll; vertikaler Drift nachfolgender Blöcke nach Konflikt mit ungleicher Zeilenzahl.
|
||||||
|
|
||||||
|
## 5. Embedded ConPTY / Mission Control
|
||||||
|
|
||||||
|
- [x] **Kritisch:** Task-basiert (Kontextmenü „Open ConPTY session"): frischer Task → Worktree wird on-demand angelegt, `claude` startet — Task-Prompt wirklich GESENDET. — **PASS (2026-07-24)**: Agent antwortete mit dem Marker `CONPTY-PROMPT-RECEIVED-4Q7`, Worktree on-demand angelegt.
|
||||||
|
- [~] Ad-hoc: „New session"-Button → Ordnerwahl → freie Session im gewählten Verzeichnis. — **funktioniert, aber Button unsichtbar**: `Icon.Plus` ist Strich-Only → `PathIcon` rendert nichts (Bug, open.md). Über Tooltip/Klick auf die leere Header-Fläche erreichbar; Ordnerwahl + Ad-hoc-Session funktionieren.
|
||||||
|
- [x] Grid↔Tabs-Toggle; Close killt den Prozess + entfernt die Kachel; mehrere Sessions parallel; Pane-Resize reflowt das TUI. — **PASS (2026-07-24)**: Resize/Reflow ok; Close entfernt Kachel UND killt den Prozess (verifiziert: ConPTY-PID 50964, Kind von ClaudeDo.App, nach Close tot).
|
||||||
|
- [~] Resume: im Worktree-Dir per `claude --continue` möglich (ClaudeDo persistiert die ConPTY-Session-Id bewusst nicht). — **wie designt**: erneutes „Open ConPTY session" resumt NICHT, sondern startet frisch und **sendet den Prompt erneut** (UX-Falle, open.md). In-App-Resume gibt es nicht; nur claude-eigenes `--continue`/`--resume` im Worktree-Dir.
|
||||||
|
|
||||||
|
## 6. Pick up in terminal
|
||||||
|
|
||||||
|
- [x] Sichtbarkeit: Kontextmenü-Eintrag + Terminal-Button (ArrowOut) NUR bei WaitingForReview und Failed; bei Idle/Running/Queued/Done nicht. — **PASS (2026-07-24, Code+Test)**: `CanPickUpInTerminal => Status is WaitingForReview or Failed` (TaskRowViewModel.cs:69, DetailsIslandViewModel.cs:898), gebunden in TaskRowView.axaml:56 + TaskHeaderBar.axaml:34, Notify bei Statuswechsel, Unit-Test `CanPickUpInTerminal_OnlyForReviewOrFailed`. (Klick→Terminal + Fehler-Surfacing bleiben manueller UI/CLI-Check.)
|
||||||
|
- [ ] Klick → neues Windows-Terminal im Worktree-Verzeichnis, `claude --resume <id>` nimmt die Session mit Kontext wieder auf.
|
||||||
|
- [ ] Fehlerfälle surfacen sauber (Footer-Strip bzw. Fehlerdialog): laufende/gequeuete Task, keine persistierte Session-Id, kein aktiver Worktree.
|
||||||
|
- Bekannte Kante: parked-Idle (reject-park) hat oft Session+Worktree, zeigt die Aktion aber bewusst NICHT (Idle nicht unterscheidbar). Nervt das in der Praxis → `CanPickUpInTerminal` erweitern.
|
||||||
|
|
||||||
|
## 7. AskUser (Frage aus laufendem Task)
|
||||||
|
|
||||||
|
Runtime-Toolname ist `mcp__claudedo_run__ask_user` (snake_case, nicht `AskUser`). Fixture: sonnet-Task mit Prompt „zwei sich widersprechende, irreversible Optionen — du MUSST `ask_user` fragen, nicht raten" erzwingt den Call zuverlässig.
|
||||||
|
|
||||||
|
- [x] Task, dessen Prompt eine Rückfrage erzwingt → Frage erscheint im Task-Monitor (Mission Control), Prozess wartet. — **PASS (2026-07-24, `verif §7 askuser 2`)**: Banner in Mission Control, Run blockiert im `ask_user`-Tool-Call. **BUG:** in der Task-Detail-Insel erscheint KEIN Banner (nur Mission Control) → s. open.md.
|
||||||
|
- [x] Antwort inline absenden → Run läuft mit der Antwort weiter. — **PASS (2026-07-24)**: Antwort „A" inline gesendet → Agent bekam „option A", benannte `merge-playground.txt` → `renamed-by-agent.txt` um, schrieb `askuser-result.txt` = „USER CHOSE: A", Task → WaitingForReview.
|
||||||
|
- [x] Timeout-/Cleanup-Verhalten der PendingQuestionRegistry (Frage unbeantwortet lassen): Task schlägt kontrolliert fehl, UI räumt die Frage auf (MCP_TOOL_TIMEOUT-Gotcha). — **PASS (2026-07-24)**: Backend (`verif §7 askuser`) nach exakt 3 min Fallback zurück, Agent hielt sich an „MUST NOT guess", meldete `CLAUDEDO_BLOCKED`, Repo untouched, Registry im `finally` aufgeräumt, Run endete `success` → WaitingForReview. **UI-Cleanup visuell bestätigt** (`verif §7 timeout-ui`, MC-Kachel offen/subscribt bis zum Timeout): Banner verschwindet beim Timeout automatisch (`TaskQuestionResolved` → `ClearPendingQuestion`).
|
||||||
|
|
||||||
|
## 8. Session Skills (E2E + UI)
|
||||||
|
|
||||||
|
- [x] Settings → Skills: `https://github.com/DietrichGebert/ponytail` installieren → 6 Skills erscheinen (ponytail, -help, -review, -audit, -debt, -gain), auf Commit gepinnt, Dateien unter `~/.todo-app/session-skills/<name>/`. — **PASS (2026-07-24)**: 6 Rows in `session_skills`, alle auf Commit `16f29800` gepinnt, `subpath=skills/<name>`, Dateien inkl. `SKILL.md` am erwarteten Ort. UI-Install nahm die URL an. **Nit:** Skills-Tab hat keinen Empty-State (s. open.md).
|
||||||
|
- [x] Skill per-Task (Agent-Settings-Flyout) oder global aktivieren → Task laufen lassen → im Worktree liegt `.claude/skills/<name>/`, Agent kann ihn nutzen, `git status` im Worktree bleibt sauber (info/exclude greift). — **PASS (2026-07-24, per-Task, `verif §8 skill-activate`)**: `ponytail-help` per Flyout aktiviert (`task.SessionSkills=["ponytail-help"]`), Run → `.claude/skills/ponytail-help/SKILL.md` im Worktree, `info/exclude` bekam `/.claude/skills/ponytail-help/`, `git status` clean, Auto-Commit enthält NUR `skill-proof.txt` (Skill NICHT im Tree). Agent hat den Skill nachweislich genutzt (`skill-proof.txt` = ponytail-help-Referenzkarte verbatim). Global-Aktivierung (AppSettings.SessionSkills, General-Tab) nutzt denselben Seeding-Union-Pfad — funktional äquivalent, nicht separat gefahren.
|
||||||
|
- [x] Gegenprobe: nicht-aktivierte/andere interaktive Session sieht den Skill NICHT (kein Leak nach `~/.claude`). — **PASS (2026-07-24)**: `~/.claude/skills/` enthält nur die vorbestehenden globalen Skills, KEIN ponytail. Seeder schreibt per Konstruktion nur nach `<workingDir>/.claude/skills` (SessionSkillSeeder), nie nach `~/.claude`.
|
||||||
|
- [~] UI: Skills-Tab (Install-Zeile, Karten mit Update/Remove), Checkbox-Listen im General-Tab + AgentConfigEditor (Flyout-Höhe!), leerer Zustand (0 Skills — Empty-State fehlt evtl., dann entscheiden), lange Namen/URLs (Trimming). — **TEILWEISE (2026-07-24)**: Install-Zeile nimmt URL an (PASS); AgentConfigEditor-Flyout-Checkbox-Liste zeigt alle 6 Skills, Höhe ok, Trimming ok (User: „funktioniert wie erhofft"). **Empty-State fehlt** (nackte Fläche — open.md). **Agent-Settings-Gear ⚙ weicht vom Listen-Gear ab** (open.md). Skills-Tab-**Karten** (Name/Beschreibung/Pinned-Commit + Update/Remove) und **General-Tab-Checkbox-Liste** (6 Skills) sichtgeprüft (User: „sieht alles gut aus"). **Remove PASS (2026-07-24)**: ein Remove-Klick auf eine ponytail-Karte entfernte alle 6 Skills per Quell-URL (`session_skills` leer, alle Verzeichnisse unter `~/.todo-app/session-skills/` weg). **Update** nicht separat gefahren (Code-Pfad `SessionSkillRegistry.UpdateAsync`).
|
||||||
|
|
||||||
|
## 9. Attachments (Drag & Drop + MCP)
|
||||||
|
|
||||||
|
- [x] Drop aufs Detail-Pane: „Drop to attach"-Overlay, Datei erscheint in der Liste, landet unter `~/.todo-app/attachments/<taskId>/`; „Add file…"-Picker; Remove-Button. — **PASS (2026-07-24, `verif §9 attachments-ui`)**: Overlay/Highlight beim Drüberziehen, Drop-Round-Trip (`drop-me.txt` in Liste + Disk + DB, 29 B), „Add file…"-Picker (`pick-me.md`), Remove (Datei aus Liste + Disk + DB) — alle bestätigt. **Finding:** der ALLERERSTE Drop der Session schlug einmalig mit inline „An error occurred" fehl (nichts persistiert), danach fehlerfrei — intermittierend, nicht reproduzierbar (s. open.md).
|
||||||
|
- [x] `ComposedPreview` enthält die Attachment-Pfade („## Reference files"). — **PASS (2026-07-24, Code+Test)**: TaskPromptComposer.cs:31 emittiert „## Reference files", ComposedPreview reicht Pfade durch, TaskRunner.cs:132 injiziert zur Laufzeit; TaskPromptComposerTests decken es ab.
|
||||||
|
- [x] MCP: `add_task_attachment` / `list_task_attachments` / `remove_task_attachment`; Running-Task verweigert add/remove. — **PASS (2026-07-24)** für add/list/remove-Round-Trip inkl. Datei unter `~/.todo-app/attachments/<taskId>/` (71 B, korrekt, nach Remove weg). Running-Task-Verweigerung ist code-guarded (AttachmentMcpTools), aber ohne dauerhaft laufende Task nicht live geprüft → manueller Rest.
|
||||||
|
|
||||||
|
## 10. Daily Prep (Prime) & Weekly Report
|
||||||
|
|
||||||
|
- [ ] Prime-Trigger: Schedule feuert bzw. „Plan day" manuell → Prep-Log streamt live, `daily-prep.log` enthält letzten Run, MyDay-Auswahl respektiert `DailyPrepMaxTasks`.
|
||||||
|
- [ ] Weekly Report: Range-Default „seit letztem Standup-Wochentag → heute", Markdown rendert, Cache pro Range.
|
||||||
|
|
||||||
|
## 11. Self-Update & Autostart (am Gerät)
|
||||||
|
|
||||||
|
- [ ] Update-Banner → Update durchführen → danach „up to date".
|
||||||
|
- [ ] Autostart: Logoff/Logon startet den Worker (Startup-`.lnk`); Update-Pfad erhält den Autostart; Uninstall entfernt die `.lnk`.
|
||||||
|
|
||||||
|
## 12. Status-Bar / RunNow (Mini-Codecheck, erst messen)
|
||||||
|
|
||||||
|
- [x] Worker trennen/verbinden → prüfen, ob RunNow-Enable pro Task-Row sauber re-evaluiert (Connection-State lebt in `IslandsShellViewModel`). Nur fixen, wenn tatsächlich kaputt. — **PASS/moot (2026-07-24, Code)**: Es gibt kein per-Row-RunNow-Control in der UI; `RunNowAsync` (IWorkerClient/WorkerClient) hat keinen VM/View-Aufrufer (RunNow nur via MCP `run_task_now`). Die realen Detail-Pane-Aktionen (Enqueue/Dequeue/Continue/ResetAndRetry) re-evaluieren korrekt bei Connection-Change (DetailsIslandViewModel.cs:317-323). Nebenbefund: `RunNowAsync` in der UI ist toter Code.
|
||||||
@@ -3,6 +3,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using ClaudeDo.Ui;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.Views;
|
using ClaudeDo.Ui.Views;
|
||||||
@@ -22,6 +23,8 @@ public partial class App : Application
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
if (_services?.GetService<AppSettings>() is { } settings)
|
||||||
|
AccentPresetService.Apply(AccentPresets.Find(settings.AccentPreset));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ sealed class Program
|
|||||||
sc.AddTransient<Func<DiffViewerViewModel>>(sp => () => sp.GetRequiredService<DiffViewerViewModel>());
|
sc.AddTransient<Func<DiffViewerViewModel>>(sp => () => sp.GetRequiredService<DiffViewerViewModel>());
|
||||||
sc.AddTransient<WorktreesOverviewModalViewModel>();
|
sc.AddTransient<WorktreesOverviewModalViewModel>();
|
||||||
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
||||||
|
sc.AddTransient<MergeHelperSelectionModalViewModel>();
|
||||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||||
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
||||||
sc.AddSingleton<IOnlineLoginService, OnlineLoginService>();
|
sc.AddSingleton<IOnlineLoginService, OnlineLoginService>();
|
||||||
@@ -137,6 +138,8 @@ sealed class Program
|
|||||||
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
||||||
sc.AddTransient<WeeklyReportModalViewModel>();
|
sc.AddTransient<WeeklyReportModalViewModel>();
|
||||||
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
||||||
|
sc.AddTransient<UsageMonitorModalViewModel>();
|
||||||
|
sc.AddTransient<Func<UsageMonitorModalViewModel>>(sp => () => sp.GetRequiredService<UsageMonitorModalViewModel>());
|
||||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||||
sp.GetRequiredService<IWorkerClient>(), taskId));
|
sp.GetRequiredService<IWorkerClient>(), taskId));
|
||||||
@@ -158,10 +161,13 @@ sealed class Program
|
|||||||
sp,
|
sp,
|
||||||
sp.GetRequiredService<INotesApi>(),
|
sp.GetRequiredService<INotesApi>(),
|
||||||
sp.GetRequiredService<IMergeCoordinator>()));
|
sp.GetRequiredService<IMergeCoordinator>()));
|
||||||
|
sc.AddSingleton<UsagePillViewModel>(sp =>
|
||||||
|
new UsagePillViewModel(sp.GetRequiredService<IWorkerClient>()));
|
||||||
sc.AddSingleton<MissionControlViewModel>(sp =>
|
sc.AddSingleton<MissionControlViewModel>(sp =>
|
||||||
new MissionControlViewModel(
|
new MissionControlViewModel(
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||||
sp.GetRequiredService<IWorkerClient>()));
|
sp.GetRequiredService<IWorkerClient>(),
|
||||||
|
sp.GetRequiredService<UsagePillViewModel>()));
|
||||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||||
{
|
{
|
||||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
|||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, IsManual (reminder only the user can do — automation skips it), Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit (nullable; review range for a worktree-less "list handler" host task — Mission Control's "Let Claude handle it" — which commits straight into the list's working dir instead of a per-task worktree: `HandlerBaseCommit` is stamped to the list repo's HEAD when the host task is created, `HandlerHeadCommit` when it's submitted for review; the Worker's `SubmitTaskForReview`/`GetTaskDiff` and the Ui's `DetailsIslandViewModel`/`MergeSectionViewModel` fall back to this pair whenever `Worktree` is null). Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`)
|
||||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns (all nullable)
|
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand (all nullable). `VerifyCommand` is an optional post-merge gate (see `ClaudeDo.Worker/CLAUDE.md` → TaskMergeService): null/blank = today's behavior, no gate.
|
||||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, MergeCommit (nullable; SHA of the merge commit this worktree's branch produced on the target branch, stamped by `TaskMergeService` the moment a merge/continue-merge succeeds — the only thing that makes `revert_merge` possible without heuristically searching `git log`; null for any worktree merged before this field existed), State (Active|Merged|Discarded|Kept)
|
||||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path, nullable `Model` — the model the run actually executed with, column `model`)
|
||||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||||
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
||||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay)
|
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100)
|
||||||
|
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and never throw — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns = 30)` always returns a usable row: `model` is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`) still hits its alias's preset row instead of missing every lookup and falling through; only a model that normalizes to nothing recognized falls back to a synthesized row (`EffortRegistry.DefaultLevel` + `fallbackMaxTurns` — callers pass `AppSettings.DefaultMaxTurns` here so that setting has a real effect instead of a hardcoded number). Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||||
|
- **ModelRegistry.TryNormalizeAlias** — non-throwing counterpart to `NormalizeAlias` for the run path: exact alias match, then substring match against a full model id, else `null`. Never throws, unlike `NormalizeAlias` (which stays the strict, throwing validator for `add_task`/planning model input).
|
||||||
|
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
|
||||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||||
|
|
||||||
## Repositories
|
## Repositories
|
||||||
@@ -22,7 +25,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
|||||||
|
|
||||||
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
||||||
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
||||||
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
|
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`, `SetMergedAsync` (atomically sets State=Merged and stamps MergeCommit in one update — the only writer of MergeCommit)
|
||||||
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||||
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
||||||
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
||||||
@@ -38,11 +41,11 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
|||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
|
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), revert (`RevertMergeCommitAsync` — `git revert --no-edit -m 1 <sha>`, reverts a merge commit against its first parent; `RevertAbortAsync`; `IsMidRevertAsync` — `REVERT_HEAD` presence, mirrors `IsMidMergeAsync`'s `MERGE_HEAD`), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo. Revert never resets/rewrites — it always produces a new commit, because the working directory it operates on is shared with other concurrent sessions.
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class ClaudeDoDbContext : DbContext
|
|||||||
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
|
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
|
||||||
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
|
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
|
||||||
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
|
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
|
||||||
|
public DbSet<SessionSkillEntity> SessionSkills => Set<SessionSkillEntity>();
|
||||||
|
|
||||||
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
|
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
|
||||||
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
builder.Property(s => s.DailyPrepMaxTasks)
|
builder.Property(s => s.DailyPrepMaxTasks)
|
||||||
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||||
|
|
||||||
|
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(s => s.ModelPresets).HasColumnName("model_presets");
|
||||||
|
|
||||||
|
builder.Property(s => s.UsageGateFiveHourPct)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct").IsRequired().HasDefaultValue(80);
|
||||||
|
builder.Property(s => s.UsageGateSevenDayPct)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct").IsRequired().HasDefaultValue(90);
|
||||||
|
|
||||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
|||||||
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
||||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||||
|
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
|||||||
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.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
|
builder.Property(l => l.IsManual).HasColumnName("is_manual").IsRequired().HasDefaultValue(false);
|
||||||
|
|
||||||
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Configuration;
|
||||||
|
|
||||||
|
public class SessionSkillEntityConfiguration : IEntityTypeConfiguration<SessionSkillEntity>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<SessionSkillEntity> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("session_skills");
|
||||||
|
|
||||||
|
builder.HasKey(s => s.Name);
|
||||||
|
builder.Property(s => s.Name).HasColumnName("name");
|
||||||
|
builder.Property(s => s.SourceUrl).HasColumnName("source_url").IsRequired();
|
||||||
|
builder.Property(s => s.PinnedRef).HasColumnName("pinned_ref").IsRequired();
|
||||||
|
builder.Property(s => s.Subpath).HasColumnName("subpath").IsRequired();
|
||||||
|
builder.Property(s => s.Description).HasColumnName("description").IsRequired();
|
||||||
|
builder.Property(s => s.AddedAt).HasColumnName("added_at").IsRequired();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,8 +89,12 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||||
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
||||||
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
||||||
|
builder.Property(t => t.IsManual).HasColumnName("is_manual").HasDefaultValue(false);
|
||||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
|
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
||||||
|
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
||||||
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class TaskRunEntityConfiguration : IEntityTypeConfiguration<TaskRunEntity
|
|||||||
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
||||||
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
||||||
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
||||||
|
builder.Property(r => r.Model).HasColumnName("model");
|
||||||
|
|
||||||
builder.HasOne(r => r.Task)
|
builder.HasOne(r => r.Task)
|
||||||
.WithMany(t => t.Runs)
|
.WithMany(t => t.Runs)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEnti
|
|||||||
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
||||||
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
|
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
|
||||||
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
||||||
|
builder.Property(w => w.MergeCommit).HasColumnName("merge_commit");
|
||||||
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
||||||
.HasDefaultValue(WorktreeState.Active)
|
.HasDefaultValue(WorktreeState.Active)
|
||||||
.HasConversion(StateConverter);
|
.HasConversion(StateConverter);
|
||||||
|
|||||||
@@ -71,9 +71,22 @@ public sealed class GitService
|
|||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
|
public Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default) =>
|
||||||
|
HasChangesAsync(worktreePath, includeUntracked: true, ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uncommitted-changes check. <paramref name="includeUntracked"/>=false ignores untracked
|
||||||
|
/// files — use this for merge preflights on a shared target working dir, where stray
|
||||||
|
/// untracked files (e.g. from a concurrent session) shouldn't block a merge. Auto-commit
|
||||||
|
/// and data-loss-guard callers keep the default (true): a new file a task created, or an
|
||||||
|
/// untracked file about to be discarded, is a real uncommitted change.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> HasChangesAsync(string worktreePath, bool includeUntracked, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
|
string[] args = includeUntracked
|
||||||
|
? ["status", "--porcelain"]
|
||||||
|
: ["status", "--porcelain", "--untracked-files=no"];
|
||||||
|
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, args, ct);
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
|
throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
|
||||||
return !string.IsNullOrWhiteSpace(stdout);
|
return !string.IsNullOrWhiteSpace(stdout);
|
||||||
@@ -267,6 +280,36 @@ public sealed class GitService
|
|||||||
throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
|
throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsMidRevertAsync(string repoDir, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["rev-parse", "--git-dir"], ct);
|
||||||
|
if (exitCode != 0) return false;
|
||||||
|
var gitDir = stdout.Trim();
|
||||||
|
if (!Path.IsPathRooted(gitDir))
|
||||||
|
gitDir = Path.Combine(repoDir, gitDir);
|
||||||
|
return File.Exists(Path.Combine(gitDir, "REVERT_HEAD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverts a single commit with `-m 1` (diff against its first parent) — the form needed to
|
||||||
|
/// revert a merge commit. On success this creates a new commit with the inverse changes;
|
||||||
|
/// the original commit and all history stay intact (no rewrite, no reset).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(int ExitCode, string Stderr)> RevertMergeCommitAsync(
|
||||||
|
string repoDir, string mergeCommitSha, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||||
|
["-c", "merge.conflictStyle=diff3", "revert", "--no-edit", "-m", "1", mergeCommitSha], ct);
|
||||||
|
return (exitCode, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertAbortAsync(string repoDir, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["revert", "--abort"], ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new InvalidOperationException($"git revert --abort failed (exit {exitCode}): {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
|
public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
||||||
@@ -330,13 +373,6 @@ public sealed class GitService
|
|||||||
.Count(s => s.Length > 0);
|
.Count(s => s.Length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MergeFfOnlyAsync(string repoDir, string branchName, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--ff-only", branchName], ct);
|
|
||||||
if (exitCode != 0)
|
|
||||||
throw new InvalidOperationException($"Fast-forward merge of '{branchName}' failed. Manual merge required. git stderr: {stderr}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
|
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
|
||||||
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
|
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,786 @@
|
|||||||
|
// <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("20260703072917_AddSessionSkills")]
|
||||||
|
partial class AddSessionSkills
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSessionSkills : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "list_config",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "session_skills",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
source_url = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
pinned_ref = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
subpath = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
description = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
added_at = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_session_skills", x => x.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "session_skills",
|
||||||
|
value: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "session_skills");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "list_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "session_skills",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+802
@@ -0,0 +1,802 @@
|
|||||||
|
// <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("20260727114206_AddModelPresetsAndManualFlag")]
|
||||||
|
partial class AddModelPresetsAndManualFlag
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddModelPresetsAndManualFlag : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "tasks",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "lists",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "model_presets",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "model_presets",
|
||||||
|
value: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_manual",
|
||||||
|
table: "lists");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "model_presets",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+810
@@ -0,0 +1,810 @@
|
|||||||
|
// <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("20260805065217_AddHandlerCommitRange")]
|
||||||
|
partial class AddHandlerCommitRange
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddHandlerCommitRange : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "handler_base_commit",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "handler_head_commit",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "handler_base_commit",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "handler_head_commit",
|
||||||
|
table: "tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+828
@@ -0,0 +1,828 @@
|
|||||||
|
// <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("20260805074906_AddUsageGateAndRunModel")]
|
||||||
|
partial class AddUsageGateAndRunModel
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUsageGateAndRunModel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "model",
|
||||||
|
table: "task_runs",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "usage_gate_five_hour_pct",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 80);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "usage_gate_seven_day_pct",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 90);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "usage_gate_five_hour_pct", "usage_gate_seven_day_pct" },
|
||||||
|
values: new object[] { 80, 90 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "model",
|
||||||
|
table: "task_runs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "usage_gate_five_hour_pct",
|
||||||
|
table: "app_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "usage_gate_seven_day_pct",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,832 @@
|
|||||||
|
// <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("20260805090016_AddVerifyCommand")]
|
||||||
|
partial class AddVerifyCommand
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("VerifyCommand")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("verify_command");
|
||||||
|
|
||||||
|
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<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddVerifyCommand : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "verify_command",
|
||||||
|
table: "list_config",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "verify_command",
|
||||||
|
table: "list_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+832
@@ -0,0 +1,832 @@
|
|||||||
|
// <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("20260805091910_AddWorktreeMergeCommit")]
|
||||||
|
partial class AddWorktreeMergeCommit
|
||||||
|
{
|
||||||
|
/// <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<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
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>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
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,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (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<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
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<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
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<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
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>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
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.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (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>("MergeCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("merge_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.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorktreeMergeCommit : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "merge_commit",
|
||||||
|
table: "worktrees",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "merge_commit",
|
||||||
|
table: "worktrees");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue(1)
|
.HasDefaultValue(1)
|
||||||
.HasColumnName("max_parallel_executions");
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
b.Property<string>("RepoImportFolders")
|
b.Property<string>("RepoImportFolders")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("repo_import_folders");
|
.HasColumnName("repo_import_folders");
|
||||||
@@ -74,12 +78,28 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("report_excluded_paths");
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
b.Property<int>("StandupWeekday")
|
b.Property<int>("StandupWeekday")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(3)
|
.HasDefaultValue(3)
|
||||||
.HasColumnName("standup_weekday");
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
b.Property<int>("WorktreeAutoCleanupDays")
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -114,6 +134,8 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
DefaultPermissionMode = "auto",
|
DefaultPermissionMode = "auto",
|
||||||
MaxParallelExecutions = 1,
|
MaxParallelExecutions = 1,
|
||||||
StandupWeekday = 3,
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
WorktreeAutoCleanupDays = 7,
|
WorktreeAutoCleanupDays = 7,
|
||||||
WorktreeAutoCleanupEnabled = false,
|
WorktreeAutoCleanupEnabled = false,
|
||||||
WorktreeStrategy = "sibling"
|
WorktreeStrategy = "sibling"
|
||||||
@@ -168,10 +190,18 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("model");
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
b.Property<string>("SystemPrompt")
|
b.Property<string>("SystemPrompt")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("system_prompt");
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("VerifyCommand")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("verify_command");
|
||||||
|
|
||||||
b.HasKey("ListId");
|
b.HasKey("ListId");
|
||||||
|
|
||||||
b.ToTable("list_config", (string)null);
|
b.ToTable("list_config", (string)null);
|
||||||
@@ -194,6 +224,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue("chore")
|
.HasDefaultValue("chore")
|
||||||
.HasColumnName("default_commit_type");
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
@@ -256,6 +292,41 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
b.ToTable("prime_schedules", (string)null);
|
b.ToTable("prime_schedules", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -363,6 +434,20 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("finished_at");
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
b.Property<bool>("IsMyDay")
|
b.Property<bool>("IsMyDay")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -437,6 +522,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("scheduled_for");
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
b.Property<int>("SortOrder")
|
b.Property<int>("SortOrder")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -509,6 +598,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("log_path");
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
b.Property<string>("Prompt")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
@@ -618,6 +711,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("head_commit");
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("MergeCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("merge_commit");
|
||||||
|
|
||||||
b.Property<string>("Path")
|
b.Property<string>("Path")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
|
|||||||
@@ -27,4 +27,15 @@ public sealed class AppSettingsEntity
|
|||||||
|
|
||||||
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||||
public int DailyPrepMaxTasks { get; set; } = 5;
|
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||||
|
|
||||||
|
// JSON array of session skill names applied by default to new tasks.
|
||||||
|
public string? SessionSkills { get; set; }
|
||||||
|
|
||||||
|
// JSON array of ModelPreset rows (model → effort + max turns). Supplies the global effort and
|
||||||
|
// turn defaults per model; list-/task-level max-turns overrides still win. Null = ship defaults.
|
||||||
|
public string? ModelPresets { get; set; }
|
||||||
|
|
||||||
|
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||||
|
public int UsageGateFiveHourPct { get; set; } = 80;
|
||||||
|
public int UsageGateSevenDayPct { get; set; } = 90;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The reasoning-effort levels the claude CLI accepts for <c>--effort</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class EffortRegistry
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyList<string> Levels = new[] { "low", "medium", "high", "xhigh", "max" };
|
||||||
|
|
||||||
|
public const string DefaultLevel = "high";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate an effort level from external input. Null/blank → null (don't pass --effort).
|
||||||
|
/// Returns the canonical lowercase level; throws on an unknown value.
|
||||||
|
/// </summary>
|
||||||
|
public static string? NormalizeLevel(string? effort)
|
||||||
|
{
|
||||||
|
var e = effort?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(e)) return null;
|
||||||
|
foreach (var level in Levels)
|
||||||
|
if (string.Equals(level, e, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return level;
|
||||||
|
throw new ArgumentException($"Unknown effort '{effort}'. Allowed: {string.Join(", ", Levels)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ public sealed class ListConfigEntity
|
|||||||
public string? SystemPrompt { get; set; }
|
public string? SystemPrompt { get; set; }
|
||||||
public string? AgentPath { get; set; }
|
public string? AgentPath { get; set; }
|
||||||
public int? MaxTurns { get; set; }
|
public int? MaxTurns { get; set; }
|
||||||
|
public string? SessionSkills { get; set; }
|
||||||
|
public string? VerifyCommand { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public ListEntity List { get; set; } = null!;
|
public ListEntity List { get; set; } = null!;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public sealed class ListEntity
|
|||||||
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; }
|
public int SortOrder { get; set; }
|
||||||
|
// A manual list holds reminders rather than Claude work: new tasks created here start out
|
||||||
|
// manual (see TaskEntity.IsManual).
|
||||||
|
public bool IsManual { get; set; }
|
||||||
|
|
||||||
// Navigation properties
|
// Navigation properties
|
||||||
public ListConfigEntity? Config { get; set; }
|
public ListConfigEntity? Config { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
/// <summary>Per-model run defaults: which reasoning effort and turn budget a session gets when
|
||||||
|
/// it runs under <paramref name="Model"/> and nothing more specific overrides it.</summary>
|
||||||
|
public sealed record ModelPreset(string Model, string Effort, int MaxTurns);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialization + lookup for <see cref="AppSettingsEntity.ModelPresets"/> (a JSON array on the
|
||||||
|
/// singleton settings row). One row per <see cref="ModelRegistry.Aliases"/> entry; the row supplies
|
||||||
|
/// the *global* effort and max-turns defaults, which list- and task-level overrides still beat.
|
||||||
|
/// </summary>
|
||||||
|
public static class ModelPresets
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
/// <summary>Shipping defaults — one row per known model alias.</summary>
|
||||||
|
public static IReadOnlyList<ModelPreset> Defaults { get; } = new[]
|
||||||
|
{
|
||||||
|
new ModelPreset("haiku", "medium", 20),
|
||||||
|
new ModelPreset("sonnet", "high", 30),
|
||||||
|
new ModelPreset("opus", "high", 40),
|
||||||
|
new ModelPreset("fable", "high", 25),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string SerializeDefaults() => Serialize(Defaults);
|
||||||
|
|
||||||
|
public static string Serialize(IEnumerable<ModelPreset> presets)
|
||||||
|
=> JsonSerializer.Serialize(Normalize(presets), Json);
|
||||||
|
|
||||||
|
/// <summary>Parses the stored JSON, falling back to <see cref="Defaults"/> for anything absent
|
||||||
|
/// or malformed — a bad settings row must never stop a run from starting.</summary>
|
||||||
|
public static IReadOnlyList<ModelPreset> Parse(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return Defaults;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<List<ModelPreset>>(json, Json);
|
||||||
|
return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return Defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
|
||||||
|
/// callers don't have to special-case an unconfigured or unknown model. <paramref name="model"/>
|
||||||
|
/// is resolved through <see cref="ModelRegistry.TryNormalizeAlias"/> first, so a full CLI model
|
||||||
|
/// id (e.g. "claude-sonnet-4-6") still hits its alias's preset row instead of falling through.
|
||||||
|
/// If <paramref name="model"/> normalizes to nothing recognized, <paramref name="fallbackMaxTurns"/>
|
||||||
|
/// (typically <c>AppSettings.DefaultMaxTurns</c>) is used instead of a hardcoded number, so a
|
||||||
|
/// truly unknown model is no harsher than the configured global default.</summary>
|
||||||
|
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model, int fallbackMaxTurns = 30)
|
||||||
|
{
|
||||||
|
var alias = ModelRegistry.TryNormalizeAlias(model) ?? (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||||
|
foreach (var p in presets)
|
||||||
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return p;
|
||||||
|
foreach (var p in Defaults)
|
||||||
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return p;
|
||||||
|
return new ModelPreset(alias, EffortRegistry.DefaultLevel, fallbackMaxTurns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModelPreset For(string? json, string? model, int fallbackMaxTurns = 30)
|
||||||
|
=> For(Parse(json), model, fallbackMaxTurns);
|
||||||
|
|
||||||
|
// One row per known alias, canonical order, clamped values — so an edited or partial list
|
||||||
|
// still round-trips into something every consumer can rely on.
|
||||||
|
private static IReadOnlyList<ModelPreset> Normalize(IEnumerable<ModelPreset> presets)
|
||||||
|
{
|
||||||
|
var byModel = new Dictionary<string, ModelPreset>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var p in presets)
|
||||||
|
{
|
||||||
|
if (p is null || string.IsNullOrWhiteSpace(p.Model)) continue;
|
||||||
|
byModel[p.Model.Trim()] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<ModelPreset>();
|
||||||
|
foreach (var fallback in Defaults)
|
||||||
|
{
|
||||||
|
var source = byModel.TryGetValue(fallback.Model, out var found) ? found : fallback;
|
||||||
|
var effort = TryNormalizeEffort(source.Effort) ?? fallback.Effort;
|
||||||
|
var turns = source.MaxTurns is >= 1 and <= 200 ? source.MaxTurns : fallback.MaxTurns;
|
||||||
|
result.Add(new ModelPreset(fallback.Model, effort, turns));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryNormalizeEffort(string? effort)
|
||||||
|
{
|
||||||
|
try { return EffortRegistry.NormalizeLevel(effort); }
|
||||||
|
catch (ArgumentException) { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ namespace ClaudeDo.Data.Models;
|
|||||||
|
|
||||||
public static class ModelRegistry
|
public static class ModelRegistry
|
||||||
{
|
{
|
||||||
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku" };
|
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku", "fable" };
|
||||||
|
|
||||||
/// <summary>Model aliases ordered cheapest → most capable. Single source for prompt cost guidance.</summary>
|
/// <summary>Model aliases ordered cheapest → most capable. Single source for prompt cost guidance.</summary>
|
||||||
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
||||||
@@ -26,4 +26,22 @@ public static class ModelRegistry
|
|||||||
return alias;
|
return alias;
|
||||||
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort alias lookup for run-path model resolution, where a full CLI model id
|
||||||
|
/// (e.g. "claude-sonnet-4-6") is just as likely as a bare alias and a run must never be
|
||||||
|
/// blocked by an unrecognized one. Null/blank/unrecognized → null; never throws.
|
||||||
|
/// </summary>
|
||||||
|
public static string? TryNormalizeAlias(string? model)
|
||||||
|
{
|
||||||
|
var m = model?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(m)) return null;
|
||||||
|
foreach (var alias in Aliases)
|
||||||
|
if (string.Equals(alias, m, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return alias;
|
||||||
|
foreach (var alias in Aliases)
|
||||||
|
if (m.Contains(alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return alias;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
public sealed class SessionSkillEntity
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required string SourceUrl { get; set; }
|
||||||
|
public required string PinnedRef { get; set; }
|
||||||
|
public required string Subpath { get; set; }
|
||||||
|
public required string Description { get; set; }
|
||||||
|
public required DateTimeOffset AddedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -43,8 +43,22 @@ public sealed class TaskEntity
|
|||||||
public int? MaxTurns { get; set; }
|
public int? MaxTurns { get; set; }
|
||||||
public bool IsStarred { get; set; }
|
public bool IsStarred { get; set; }
|
||||||
public bool IsMyDay { get; set; }
|
public bool IsMyDay { get; set; }
|
||||||
|
// Manual = a reminder only the user can do. Automation skips it (queue picker, daily prep,
|
||||||
|
// list handler) and the Claude affordances are hidden; a hand-driven ConPTY session is still
|
||||||
|
// allowed. New tasks in a manual list default to true.
|
||||||
|
public bool IsManual { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
|
public string? SessionSkills { get; set; }
|
||||||
|
|
||||||
|
// Review range for a worktree-less task hosting an interactive "list handler" run
|
||||||
|
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
||||||
|
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
||||||
|
// capture the repo's HEAD at session start / submit-for-review instead, so the normal
|
||||||
|
// diff/get_task_diff paths can show `HandlerBaseCommit..HandlerHeadCommit` over the
|
||||||
|
// list's working dir exactly like a merged task's commit-range diff.
|
||||||
|
public string? HandlerBaseCommit { get; set; }
|
||||||
|
public string? HandlerHeadCommit { get; set; }
|
||||||
|
|
||||||
public string? ParentTaskId { get; set; }
|
public string? ParentTaskId { get; set; }
|
||||||
public string? PlanningSessionId { get; set; }
|
public string? PlanningSessionId { get; set; }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public sealed class TaskRunEntity
|
|||||||
public string? LogPath { get; set; }
|
public string? LogPath { get; set; }
|
||||||
public DateTime? StartedAt { get; set; }
|
public DateTime? StartedAt { get; set; }
|
||||||
public DateTime? FinishedAt { get; set; }
|
public DateTime? FinishedAt { get; set; }
|
||||||
|
public string? Model { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public TaskEntity Task { get; set; } = null!;
|
public TaskEntity Task { get; set; } = null!;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class WorktreeEntity
|
|||||||
public required string BaseCommit { get; set; }
|
public required string BaseCommit { get; set; }
|
||||||
public string? HeadCommit { get; set; }
|
public string? HeadCommit { get; set; }
|
||||||
public string? DiffStat { get; set; }
|
public string? DiffStat { get; set; }
|
||||||
|
public string? MergeCommit { get; set; }
|
||||||
public WorktreeState State { get; set; } = WorktreeState.Active;
|
public WorktreeState State { get; set; } = WorktreeState.Active;
|
||||||
public required DateTime CreatedAt { get; init; }
|
public required DateTime CreatedAt { get; init; }
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace ClaudeDo.Data;
|
namespace ClaudeDo.Data;
|
||||||
|
|
||||||
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine }
|
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelper, MergeHelperInitial }
|
||||||
|
|
||||||
public static class PromptFiles
|
public static class PromptFiles
|
||||||
{
|
{
|
||||||
@@ -18,6 +18,8 @@ public static class PromptFiles
|
|||||||
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||||
PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.md"),
|
PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.md"),
|
||||||
PromptKind.Refine => Path.Combine(Root, "refine.md"),
|
PromptKind.Refine => Path.Combine(Root, "refine.md"),
|
||||||
|
PromptKind.MergeHelper => Path.Combine(Root, "merge-helper-system.md"),
|
||||||
|
PromptKind.MergeHelperInitial => Path.Combine(Root, "merge-helper-initial.md"),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,6 +65,8 @@ public static class PromptFiles
|
|||||||
PromptKind.WeeklyReport => WeeklyReportDefault,
|
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||||
PromptKind.ImprovementChild => ImprovementChildDefault,
|
PromptKind.ImprovementChild => ImprovementChildDefault,
|
||||||
PromptKind.Refine => RefineDefault,
|
PromptKind.Refine => RefineDefault,
|
||||||
|
PromptKind.MergeHelper => MergeHelperDefault,
|
||||||
|
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,6 +168,12 @@ public static class PromptFiles
|
|||||||
- sonnet — normal coding work; the sensible default when unsure.
|
- sonnet — normal coding work; the sensible default when unsure.
|
||||||
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
||||||
Do not default everything to opus — most subtasks are haiku or sonnet.
|
Do not default everything to opus — most subtasks are haiku or sonnet.
|
||||||
|
|
||||||
|
Only for a subtask you can tell will need noticeably more turns than the
|
||||||
|
default budget (a large or sprawling piece of work), also pass CreateChildTask's
|
||||||
|
`maxTurns` argument with a generous turn count, so the run doesn't die mid-work
|
||||||
|
at the turn limit. Leave `maxTurns` null for everything else — it inherits the
|
||||||
|
list/global default.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private const string PlanningInitialDefault = """
|
private const string PlanningInitialDefault = """
|
||||||
@@ -224,6 +234,99 @@ public static class PromptFiles
|
|||||||
task, stop.
|
task, stop.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
private const string MergeHelperDefault = """
|
||||||
|
You are the ClaudeDo list handler, running as an interactive session with the user watching. Work autonomously and decide things yourself by default. Ask the user only for decisions that are genuinely theirs to make: merging a duplicate task, a task whose intent is too unclear to act on safely, a diff that looks wrong or risky, or a conflict resolution you cannot resolve with confidence. Everything else, decide and keep moving.
|
||||||
|
|
||||||
|
Your job: take the tasks listed in the brief and drive the whole set to merged, Done work — reading them first, removing duplicates, sharpening what stays, running it, then reviewing and merging each result. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
||||||
|
|
||||||
|
Work the five phases in order. Do not start a phase before the previous one is finished.
|
||||||
|
|
||||||
|
## Phase 0 — Read everything
|
||||||
|
The brief is the primary source: it already lists every task's title, id, status and full description. Read it in full before acting. Only call batch_get_tasks if you need something the brief does not carry for a specific task, e.g. parent/child links. Do not act on any single task before you have read them all — Phase 1 needs the whole set in view.
|
||||||
|
|
||||||
|
## Phase 1 — Dedupe
|
||||||
|
Compare the tasks pairwise for overlap: same goal stated twice, one task fully contained in another, two tasks that would edit the same thing for the same reason.
|
||||||
|
|
||||||
|
If no pair looks like a duplicate, say so in one sentence and move straight to Phase 2 — do not ask the user to confirm the absence of duplicates.
|
||||||
|
|
||||||
|
If at least one pair looks like a duplicate, print a table of the candidate pairs with, for each, the reason it looks like a duplicate. Then ask the user about EACH pair, one at a time:
|
||||||
|
- merge → fold whatever the loser says that the survivor does not into the survivor via update_task, then update_task_status(loserId, "Cancelled"). Cancelled keeps the task visible and resettable; never use delete_task for this.
|
||||||
|
- keep both → note why and move on.
|
||||||
|
|
||||||
|
Cancel nothing without an explicit answer.
|
||||||
|
|
||||||
|
## Phase 2 — Enhance for execution
|
||||||
|
Each surviving task is about to be run by an autonomous agent with no further input. Sharpen it so that run can succeed. For each task, rewrite title and description to carry:
|
||||||
|
- concrete acceptance criteria — what must be true when it is done,
|
||||||
|
- the files and areas actually involved, found with Read/Grep/Glob in the repo. Do not guess paths; look them up.
|
||||||
|
- what is explicitly out of scope.
|
||||||
|
|
||||||
|
Write it back with update_task (title, description and commitType are the settable fields).
|
||||||
|
|
||||||
|
Rules: do not change what the user asked for, and do not invent requirements. You are making the existing intent precise, not adding to it. If a task is too vague to sharpen without guessing, ASK instead of guessing. Report a short before/after per task.
|
||||||
|
|
||||||
|
If a task visibly bundles several independent features, or has a blocker that is not resolved by anything in its own description, do not force it into one description. Propose splitting it to the user; if they agree, create the pieces with add_task/add_subtask and only move the pieces the user confirmed into "surviving tasks" for the phases below. Split only what the task already asks for — the "do not invent requirements" rule still applies.
|
||||||
|
|
||||||
|
## Phase 3 — Run
|
||||||
|
Do NOT use run_task_now for a batch — there is a single override slot and the second call fails with "override slot busy".
|
||||||
|
|
||||||
|
Read get_app_settings and tell the user how many parallel execution slots are configured (maxParallelExecutions). If it is 1, say plainly that the tasks will execute one after another and that the value is changeable in ClaudeDo's settings.
|
||||||
|
|
||||||
|
For each surviving task, resolve the effective max-turns it will run with — task.MaxTurns, else the list's get_list_config MaxTurns, else the model's preset/global default — and report it. If a task looks substantial (several files, or one you just split off above) but its effective turns look low, say so and ask before queuing it; set_task_config/get_task_config let you raise it per task.
|
||||||
|
|
||||||
|
Then, for each surviving task:
|
||||||
|
- Idle or Failed → update_task_status(id, "Queued"). For a Failed task ask first whether to reset_failed_task and re-queue it, or skip it.
|
||||||
|
- Queued → leave it; it is already waiting for a slot.
|
||||||
|
- Running or WaitingForChildren → leave it; only poll.
|
||||||
|
- WaitingForReview → leave it; it goes straight to Phase 4.
|
||||||
|
|
||||||
|
Call wait_for_task_change with the ids of every task still Queued or Running (timeoutSeconds up to 170) instead of sleeping and polling get_task yourself. It returns as soon as any of them leaves Queued/Running — WaitingForReview on success, Failed on error — or reports timedOut if none did. Report progress as tasks land, then call it again with whatever ids are still Queued/Running until none remain.
|
||||||
|
|
||||||
|
## Phase 4 — Review and merge
|
||||||
|
Before merging anything, call preview_merge_set with every surviving task's id (the same targetBranch you are about to merge into). It tells you, per task, whether a clean merge-tree preview is even possible (status/conflictFiles/changedFileCount/behind) and which files more than one of the tasks changed (overlaps). Read the overlaps: a file two tasks both touch is where a same-branch collision could happen. This is a HINT, not proof — it only catches same-file overlap, not a cross-file break (e.g. one task deletes a symbol another task's file still references), and a clean preview never guarantees the result compiles or passes tests. Use it to decide merge order and to know which pairs to look at extra carefully in step 1 below; it does not replace reading the diffs.
|
||||||
|
|
||||||
|
One task at a time. Default to the order the brief lists them. If the overlap check above (or the file lists you gathered in Phase 2) shows two or more tasks touching the same file, tell the user which tasks collide and merge those in an order you can justify (e.g. the one making the smaller change first) — deviate from brief order only with that stated reason.
|
||||||
|
|
||||||
|
1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title and description. If preview_merge_set flagged this task in an overlap, also skim the diff of the other task(s) sharing that file.
|
||||||
|
2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
|
||||||
|
3. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||||
|
- Clean merge → the task is Done; move on.
|
||||||
|
- Conflict (markers left in the working tree, repoPath returned) → resolve it.
|
||||||
|
|
||||||
|
After each merge, main has moved — a clean preview_merge_set result from before this merge is now stale for the remaining tasks. If you are unsure whether an earlier overlap warning still matters, call preview_merge for the next task again before merging it.
|
||||||
|
|
||||||
|
Every branch in this run forked from the same base, so conflicts between them are the NORMAL case, not a failure. Resolve them and keep going; do not abandon the run because a merge conflicted.
|
||||||
|
|
||||||
|
Resolving a conflict:
|
||||||
|
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you — fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
|
||||||
|
- For a task WITH children (a unit merge), pass the PARENT task id to continue_merge / abort_merge.
|
||||||
|
- If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing.
|
||||||
|
- Prefer the MCP tools whenever they apply. Only if the MCP tools cannot reach an in-progress merge may you finish it by hand: resolve the markers, then `git add -- <the resolved paths>` and `git commit` — NEVER `git add -A` or a bare commit, because the checkout is shared with other sessions.
|
||||||
|
|
||||||
|
Rules for the whole session:
|
||||||
|
- Never use raw `git merge`, `git reset`, or `git checkout` to force a merge. Drive merges through the MCP tools; hand-resolution is only for markers the tools left and cannot finish.
|
||||||
|
- Ask the user for anything ambiguous, risky, or destructive.
|
||||||
|
|
||||||
|
## Phase 5 — Summary
|
||||||
|
Print one line per task from the original brief:
|
||||||
|
title — dedupe action (kept / merged into X / cancelled as duplicate of X) — enhanced (yes/no) — final status — merge commit (if any) — conflicts resolved (if any).
|
||||||
|
|
||||||
|
Then list anything you skipped or left for the user and why, and any follow-ups worth turning into new tasks.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string MergeHelperInitialDefault = """
|
||||||
|
# List handler brief
|
||||||
|
|
||||||
|
Scope: {scope}
|
||||||
|
Repo: {repo}
|
||||||
|
|
||||||
|
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
||||||
|
|
||||||
|
{tasks}
|
||||||
|
|
||||||
|
When every task is handled, print the summary.
|
||||||
|
""";
|
||||||
|
|
||||||
private const string WeeklyReportDefault = """
|
private const string WeeklyReportDefault = """
|
||||||
You are generating a concise weekly standup report for a software developer,
|
You are generating a concise weekly standup report for a software developer,
|
||||||
covering {start} to {end}.
|
covering {start} to {end}.
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ public sealed class AppSettingsRepository
|
|||||||
? null : updated.ReportExcludedPaths;
|
? null : updated.ReportExcludedPaths;
|
||||||
row.StandupWeekday = updated.StandupWeekday;
|
row.StandupWeekday = updated.StandupWeekday;
|
||||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
|
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
||||||
|
row.UsageGateFiveHourPct = Math.Clamp(updated.UsageGateFiveHourPct, 0, 100);
|
||||||
|
row.UsageGateSevenDayPct = Math.Clamp(updated.UsageGateSevenDayPct, 0, 100);
|
||||||
|
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ public sealed class ListRepository
|
|||||||
existing.SystemPrompt = config.SystemPrompt;
|
existing.SystemPrompt = config.SystemPrompt;
|
||||||
existing.AgentPath = config.AgentPath;
|
existing.AgentPath = config.AgentPath;
|
||||||
existing.MaxTurns = config.MaxTurns;
|
existing.MaxTurns = config.MaxTurns;
|
||||||
|
existing.SessionSkills = config.SessionSkills;
|
||||||
|
existing.VerifyCommand = config.VerifyCommand;
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Repositories;
|
||||||
|
|
||||||
|
public sealed class SessionSkillRepository
|
||||||
|
{
|
||||||
|
private readonly ClaudeDoDbContext _context;
|
||||||
|
|
||||||
|
public SessionSkillRepository(ClaudeDoDbContext context) => _context = context;
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SessionSkillEntity>> ListAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _context.SessionSkills.AsNoTracking()
|
||||||
|
.OrderBy(s => s.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SessionSkillEntity?> GetAsync(string name, CancellationToken ct = default) =>
|
||||||
|
await _context.SessionSkills.AsNoTracking().FirstOrDefaultAsync(s => s.Name == name, ct);
|
||||||
|
|
||||||
|
public async Task UpsertAsync(SessionSkillEntity entity, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var existing = await _context.SessionSkills.FirstOrDefaultAsync(s => s.Name == entity.Name, ct);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
_context.SessionSkills.Add(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.SourceUrl = entity.SourceUrl;
|
||||||
|
existing.PinnedRef = entity.PinnedRef;
|
||||||
|
existing.Subpath = entity.Subpath;
|
||||||
|
existing.Description = entity.Description;
|
||||||
|
existing.AddedAt = entity.AddedAt;
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string name, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.SessionSkills
|
||||||
|
.Where(s => s.Name == name)
|
||||||
|
.ExecuteDeleteAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteBySourceAsync(string sourceUrl, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.SessionSkills
|
||||||
|
.Where(s => s.SourceUrl == sourceUrl)
|
||||||
|
.ExecuteDeleteAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SessionSkillEntity>> ListBySourceAsync(string sourceUrl, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _context.SessionSkills.AsNoTracking()
|
||||||
|
.Where(s => s.SourceUrl == sourceUrl)
|
||||||
|
.OrderBy(s => s.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,60 @@ public sealed class TaskRepository
|
|||||||
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
||||||
=> GetByListIdAsync(listId, ct);
|
=> GetByListIdAsync(listId, ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the ids of every descendant of <paramref name="taskId"/> (children, grandchildren, ...),
|
||||||
|
/// walking the ParentTaskId chain breadth-first. Does not include the task itself.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<string>> GetDescendantIdsAsync(string taskId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
// Guards against a corrupt parent chain (a self-parent or a cycle would loop forever).
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal) { taskId };
|
||||||
|
var frontier = new List<string> { taskId };
|
||||||
|
while (frontier.Count > 0)
|
||||||
|
{
|
||||||
|
var children = await _context.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.ParentTaskId != null && frontier.Contains(t.ParentTaskId))
|
||||||
|
.Select(t => t.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var fresh = children.Where(seen.Add).ToList();
|
||||||
|
if (fresh.Count == 0) break;
|
||||||
|
result.AddRange(fresh);
|
||||||
|
frontier = fresh;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a task (and every descendant, so a child never ends up in a different list than its
|
||||||
|
/// parent) to <paramref name="targetListId"/>, appending it at the end of the target list's
|
||||||
|
/// order. ListId is init-only, so the move goes through ExecuteUpdate rather than a tracked
|
||||||
|
/// entity mutation.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MoveToListAsync(string taskId, string targetListId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var exists = await _context.Tasks.AsNoTracking().AnyAsync(t => t.Id == taskId, ct);
|
||||||
|
if (!exists)
|
||||||
|
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
var descendantIds = await GetDescendantIdsAsync(taskId, ct);
|
||||||
|
var movedIds = new List<string> { taskId };
|
||||||
|
movedIds.AddRange(descendantIds);
|
||||||
|
|
||||||
|
var maxSort = await _context.Tasks
|
||||||
|
.Where(t => t.ListId == targetListId)
|
||||||
|
.Select(t => (int?)t.SortOrder)
|
||||||
|
.MaxAsync(ct);
|
||||||
|
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => movedIds.Contains(t.Id))
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.ListId, targetListId), ct);
|
||||||
|
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SortOrder, (maxSort ?? -1) + 1), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
@@ -189,6 +243,7 @@ public sealed class TaskRepository
|
|||||||
string? systemPrompt,
|
string? systemPrompt,
|
||||||
string? agentPath,
|
string? agentPath,
|
||||||
int? maxTurns = null,
|
int? maxTurns = null,
|
||||||
|
string? sessionSkills = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Tasks
|
await _context.Tasks
|
||||||
@@ -197,7 +252,8 @@ public sealed class TaskRepository
|
|||||||
.SetProperty(t => t.Model, model)
|
.SetProperty(t => t.Model, model)
|
||||||
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
||||||
.SetProperty(t => t.AgentPath, agentPath)
|
.SetProperty(t => t.AgentPath, agentPath)
|
||||||
.SetProperty(t => t.MaxTurns, maxTurns), ct);
|
.SetProperty(t => t.MaxTurns, maxTurns)
|
||||||
|
.SetProperty(t => t.SessionSkills, sessionSkills), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -220,8 +276,12 @@ public sealed class TaskRepository
|
|||||||
string? commitType,
|
string? commitType,
|
||||||
string? createdBy = null,
|
string? createdBy = null,
|
||||||
string? model = null,
|
string? model = null,
|
||||||
|
int? maxTurns = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
if (maxTurns.HasValue && maxTurns.Value <= 0)
|
||||||
|
throw new ArgumentException($"maxTurns must be positive, got {maxTurns.Value}.");
|
||||||
|
|
||||||
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
||||||
// bypasses the change tracker; a tracked Find would return stale data.
|
// bypasses the change tracker; a tracked Find would return stale data.
|
||||||
var parent = await _context.Tasks.AsNoTracking()
|
var parent = await _context.Tasks.AsNoTracking()
|
||||||
@@ -247,6 +307,7 @@ public sealed class TaskRepository
|
|||||||
SortOrder = (maxSort ?? -1) + 1,
|
SortOrder = (maxSort ?? -1) + 1,
|
||||||
CreatedBy = createdBy,
|
CreatedBy = createdBy,
|
||||||
Model = ModelRegistry.NormalizeAlias(model),
|
Model = ModelRegistry.NormalizeAlias(model),
|
||||||
|
MaxTurns = maxTurns,
|
||||||
};
|
};
|
||||||
_context.Tasks.Add(child);
|
_context.Tasks.Add(child);
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
@@ -328,6 +389,19 @@ public sealed class TaskRepository
|
|||||||
.SetProperty(t => t.PlanningSessionId, sessionId), ct);
|
.SetProperty(t => t.PlanningSessionId, sessionId), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stamps the review range's head commit for a worktree-less "list handler" host task
|
||||||
|
// (Mission Control's "Let Claude handle it") when its ConPTY session is submitted for review.
|
||||||
|
public async Task SetHandlerHeadCommitAsync(
|
||||||
|
string taskId,
|
||||||
|
string headCommit,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s
|
||||||
|
.SetProperty(t => t.HandlerHeadCommit, headCommit), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TaskEntity?> FindByPlanningTokenAsync(
|
public async Task<TaskEntity?> FindByPlanningTokenAsync(
|
||||||
string token,
|
string token,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ public sealed class WorktreeRepository
|
|||||||
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetMergedAsync(string taskId, string mergeCommit, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.Worktrees
|
||||||
|
.Where(w => w.TaskId == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s
|
||||||
|
.SetProperty(w => w.State, WorktreeState.Merged)
|
||||||
|
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// The app relaunches us via ShellExecute without an explicit working directory, so we
|
||||||
|
// inherit its CWD — which the Start Menu shortcut sets to <InstallDir>\app. A process's
|
||||||
|
// current directory is locked by the OS, so we would block DownloadAndExtractStep's
|
||||||
|
// Directory.Move of app\ against ourselves (fails on every attempt, reboot included).
|
||||||
|
// Step out of the install tree before anything else touches it.
|
||||||
|
try { Environment.CurrentDirectory = Path.GetTempPath(); } catch { /* best effort */ }
|
||||||
|
|
||||||
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
|
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
|
||||||
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
||||||
var localeStore = LocaleStore.Load(localesDir);
|
var localeStore = LocaleStore.Load(localesDir);
|
||||||
@@ -124,7 +131,7 @@ public partial class App : Application
|
|||||||
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
||||||
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
||||||
// can pull them out individually via GetRequiredService<T>().
|
// can pull them out individually via GetRequiredService<T>().
|
||||||
sc.AddSingleton<DownloadAndExtractStep>();
|
sc.AddSingleton(sp => new DownloadAndExtractStep(sp.GetRequiredService<IReleaseClient>()));
|
||||||
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>();
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ Non-fatal if `claude` CLI is missing or too old (prints the manual command). Ser
|
|||||||
|
|
||||||
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
||||||
|
|
||||||
|
**`DownloadAndExtractStep`** — fetches `checksums.txt` first and only touches the install dir after the zip verifies. The zip is cached in `%TEMP%\ClaudeDo-download-cache` (ctor takes an override for tests) and reused on a retry when its SHA-256 still matches, so a failed attempt doesn't cost another full download; it is dropped after a successful install, a bad download is deleted immediately, and zips of other versions are pruned. `app\`/`worker\` are stashed to `*.bak` before extraction and restored if extraction fails.
|
||||||
|
|
||||||
|
### Gotcha: the installer must never run from inside the install dir
|
||||||
|
|
||||||
|
`App.OnStartup` sets `Environment.CurrentDirectory` to `%TEMP%`, and the UI passes an explicit `WorkingDirectory` when it relaunches us. A process's current directory is locked by Windows: inheriting the app's CWD (`<InstallDir>\app`, from the Start Menu shortcut's "start in") made the installer block its own `app` → `app.bak` rename, so every update failed with "Could not replace the existing files" — unaffected by retries or a reboot. Keep both guards.
|
||||||
|
|
||||||
## `InstallContext` Defaults
|
## `InstallContext` Defaults
|
||||||
|
|
||||||
| Property | Default |
|
| Property | Default |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Threading;
|
||||||
using ClaudeDo.Installer.Core;
|
using ClaudeDo.Installer.Core;
|
||||||
using ClaudeDo.Releases;
|
using ClaudeDo.Releases;
|
||||||
|
|
||||||
@@ -8,10 +9,14 @@ namespace ClaudeDo.Installer.Steps;
|
|||||||
public sealed class DownloadAndExtractStep : IInstallStep
|
public sealed class DownloadAndExtractStep : IInstallStep
|
||||||
{
|
{
|
||||||
private readonly IReleaseClient _releases;
|
private readonly IReleaseClient _releases;
|
||||||
|
private readonly string _cacheDir;
|
||||||
|
|
||||||
public DownloadAndExtractStep(IReleaseClient releases)
|
public DownloadAndExtractStep(IReleaseClient releases, string? cacheDirectory = null)
|
||||||
{
|
{
|
||||||
_releases = releases;
|
_releases = releases;
|
||||||
|
// Downloads survive a failed attempt so a retry doesn't pull ~100 MB again.
|
||||||
|
// %TEMP% because Storage Sense ages the cache out on its own.
|
||||||
|
_cacheDir = cacheDirectory ?? Path.Combine(Path.GetTempPath(), "ClaudeDo-download-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Download and Extract";
|
public string Name => "Download and Extract";
|
||||||
@@ -37,14 +42,28 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
if (checksumAsset is null)
|
if (checksumAsset is null)
|
||||||
return StepResult.Fail("checksums.txt not found in release metadata.");
|
return StepResult.Fail("checksums.txt not found in release metadata.");
|
||||||
|
|
||||||
var scratchDir = Path.Combine(Path.GetTempPath(), "ClaudeDo-install-" + Guid.NewGuid().ToString("N"));
|
Directory.CreateDirectory(_cacheDir);
|
||||||
Directory.CreateDirectory(scratchDir);
|
var zipPath = Path.Combine(_cacheDir, zipAsset.Name);
|
||||||
|
var checksumPath = Path.Combine(_cacheDir, "checksums.txt");
|
||||||
|
PruneCacheExcept(zipAsset.Name);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
progress.Report("Downloading checksums...");
|
||||||
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
||||||
|
new Progress<long>(_ => { }), ct);
|
||||||
|
|
||||||
|
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
||||||
|
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
||||||
|
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
||||||
|
|
||||||
|
// An earlier attempt may have failed after the download (locked files, bad
|
||||||
|
// extraction). Reuse that zip when it still verifies rather than re-downloading.
|
||||||
|
if (File.Exists(zipPath) && ChecksumVerifier.Verify(zipPath, expectedHash))
|
||||||
|
{
|
||||||
|
progress.Report($"Reusing the already downloaded {zipAsset.Name}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var totalMb = zipAsset.Size / (1024 * 1024);
|
var totalMb = zipAsset.Size / (1024 * 1024);
|
||||||
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
||||||
long lastReportedMb = -1;
|
long lastReportedMb = -1;
|
||||||
@@ -59,16 +78,14 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
}),
|
}),
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
progress.Report("Downloading checksums...");
|
|
||||||
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
|
||||||
new Progress<long>(_ => { }), ct);
|
|
||||||
|
|
||||||
progress.Report("Verifying checksum...");
|
progress.Report("Verifying checksum...");
|
||||||
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
|
||||||
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
|
||||||
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
|
||||||
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
||||||
|
{
|
||||||
|
// Never keep a bad download around — it would be re-verified forever.
|
||||||
|
TryDelete(zipPath);
|
||||||
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only after verification do we touch the install directory.
|
// Only after verification do we touch the install directory.
|
||||||
progress.Report("Stashing previous app/worker binaries...");
|
progress.Report("Stashing previous app/worker binaries...");
|
||||||
@@ -77,10 +94,34 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
var appBak = appDest + ".bak";
|
var appBak = appDest + ".bak";
|
||||||
var workerBak = workerDest + ".bak";
|
var workerBak = workerDest + ".bak";
|
||||||
|
|
||||||
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
var stashedApp = false;
|
||||||
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
var failedPath = ctx.InstallDirectory;
|
||||||
if (Directory.Exists(appDest)) Directory.Move(appDest, appBak);
|
try
|
||||||
if (Directory.Exists(workerDest)) Directory.Move(workerDest, workerBak);
|
{
|
||||||
|
failedPath = appBak;
|
||||||
|
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||||
|
failedPath = workerBak;
|
||||||
|
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||||
|
failedPath = appDest;
|
||||||
|
if (Directory.Exists(appDest)) { MoveWithRetry(appDest, appBak); stashedApp = true; }
|
||||||
|
failedPath = workerDest;
|
||||||
|
if (Directory.Exists(workerDest)) MoveWithRetry(workerDest, workerBak);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
// Undo a half-done stash: a leftover app.bak would be deleted as a stale
|
||||||
|
// stash on the next attempt — that copy is the only one we still have.
|
||||||
|
if (stashedApp && !Directory.Exists(appDest) && Directory.Exists(appBak))
|
||||||
|
try { MoveWithRetry(appBak, appDest); } catch { /* best effort */ }
|
||||||
|
|
||||||
|
// A just-stopped app/worker (or an Explorer/terminal window sitting in
|
||||||
|
// the install dir) still held a handle. Surface an actionable message
|
||||||
|
// instead of the raw "process cannot access the file" error.
|
||||||
|
return StepResult.Fail(
|
||||||
|
$"Could not replace '{failedPath}' — it is still in use. " +
|
||||||
|
"Make sure ClaudeDo is fully closed (app and worker) and no Explorer or " +
|
||||||
|
$"terminal window is open inside the install folder, then run the update again. Details: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
progress.Report("Extracting...");
|
progress.Report("Extracting...");
|
||||||
Directory.CreateDirectory(ctx.InstallDirectory);
|
Directory.CreateDirectory(ctx.InstallDirectory);
|
||||||
@@ -91,24 +132,65 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Roll back to previous binaries.
|
// Roll back to previous binaries.
|
||||||
if (Directory.Exists(appDest)) Directory.Delete(appDest, recursive: true);
|
if (Directory.Exists(appDest)) DeleteWithRetry(appDest);
|
||||||
if (Directory.Exists(workerDest)) Directory.Delete(workerDest, recursive: true);
|
if (Directory.Exists(workerDest)) DeleteWithRetry(workerDest);
|
||||||
if (Directory.Exists(appBak)) Directory.Move(appBak, appDest);
|
if (Directory.Exists(appBak)) MoveWithRetry(appBak, appDest);
|
||||||
if (Directory.Exists(workerBak)) Directory.Move(workerBak, workerDest);
|
if (Directory.Exists(workerBak)) MoveWithRetry(workerBak, workerDest);
|
||||||
return StepResult.Fail(
|
return StepResult.Fail(
|
||||||
$"Extraction failed; previous binaries have been restored: {ex.Message}.");
|
$"Extraction failed; previous binaries have been restored: {ex.Message}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success — drop stash.
|
// Success — drop stash.
|
||||||
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||||
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||||
|
|
||||||
|
// Installed — the cached zip has served its purpose.
|
||||||
|
TryDelete(zipPath);
|
||||||
|
TryDelete(checksumPath);
|
||||||
|
|
||||||
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
||||||
return StepResult.Ok();
|
return StepResult.Ok();
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
|
||||||
|
// Zips from earlier attempts on other versions would pile up otherwise.
|
||||||
|
private void PruneCacheExcept(string keepFileName)
|
||||||
{
|
{
|
||||||
try { Directory.Delete(scratchDir, recursive: true); } catch { /* best effort */ }
|
try
|
||||||
|
{
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_cacheDir, "*.zip"))
|
||||||
|
{
|
||||||
|
if (!string.Equals(Path.GetFileName(file), keepFileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
TryDelete(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string file)
|
||||||
|
{
|
||||||
|
try { File.Delete(file); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MoveWithRetry(string source, string dest)
|
||||||
|
=> RetryIo(() => Directory.Move(source, dest));
|
||||||
|
|
||||||
|
private static void DeleteWithRetry(string dir)
|
||||||
|
=> RetryIo(() => Directory.Delete(dir, recursive: true));
|
||||||
|
|
||||||
|
// WaitForExit returns before Windows releases a just-killed process's file
|
||||||
|
// handles, so the stash Move/Delete can briefly hit a sharing violation.
|
||||||
|
// Retry through transient IO/access errors (~5s) before letting it surface.
|
||||||
|
private static void RetryIo(Action action)
|
||||||
|
{
|
||||||
|
const int attempts = 10;
|
||||||
|
for (var i = 0; ; i++)
|
||||||
|
{
|
||||||
|
try { action(); return; }
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException && i < attempts - 1)
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,33 @@ public sealed class StopWorkerStep : IInstallStep
|
|||||||
{
|
{
|
||||||
progress.Report("Stopping ClaudeDo processes (if running)...");
|
progress.Report("Stopping ClaudeDo processes (if running)...");
|
||||||
var installDir = ctx.InstallDirectory;
|
var installDir = ctx.InstallDirectory;
|
||||||
|
var killedAny = false;
|
||||||
foreach (var name in ProcessNames)
|
foreach (var name in ProcessNames)
|
||||||
{
|
{
|
||||||
foreach (var p in Process.GetProcessesByName(name))
|
foreach (var p in Process.GetProcessesByName(name))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = p.MainModule?.FileName;
|
// Scope to THIS install when the module path is readable; if it
|
||||||
|
// can't be read (access race / exiting process), fall through and
|
||||||
|
// kill anyway — a survivor would lock the install dir during
|
||||||
|
// extraction. Reading MainModule must not skip the Kill.
|
||||||
|
string? path = null;
|
||||||
|
try { path = p.MainModule?.FileName; } catch { /* unreadable — kill anyway */ }
|
||||||
if (path is not null && !IsUnder(path, installDir)) continue;
|
if (path is not null && !IsUnder(path, installDir)) continue;
|
||||||
p.Kill(entireProcessTree: true);
|
p.Kill(entireProcessTree: true);
|
||||||
p.WaitForExit(10000);
|
p.WaitForExit(10000);
|
||||||
|
killedAny = true;
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
|
// WaitForExit returns before the OS releases the process's file handles.
|
||||||
|
// Give it a moment so DownloadAndExtractStep's Directory.Move doesn't race
|
||||||
|
// a still-open handle. (That step also retries, this just avoids the churn.)
|
||||||
|
if (killedAny) await Task.Delay(1500, ct);
|
||||||
return StepResult.Ok();
|
return StepResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,22 @@
|
|||||||
"tabWorktrees": "Worktrees",
|
"tabWorktrees": "Worktrees",
|
||||||
"tabFiles": "Dateien",
|
"tabFiles": "Dateien",
|
||||||
"tabPrime": "Prime Claude",
|
"tabPrime": "Prime Claude",
|
||||||
|
"tabSkills": "Skills",
|
||||||
"general": {
|
"general": {
|
||||||
"defaultInstructions": "Standard-Anweisungen",
|
"defaultInstructions": "Standard-Anweisungen",
|
||||||
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
||||||
"model": "Modell",
|
"model": "Modell",
|
||||||
"maxTurns": "Max. Durchläufe",
|
"maxTurns": "Max. Durchläufe",
|
||||||
|
"effort": "Effort",
|
||||||
|
"modelPresets": "Vorgaben pro Modell",
|
||||||
|
"modelPresetsHint": "Effort und Durchlauf-Budget, mit denen eine Sitzung unter diesem Modell läuft. Overrides auf Listen- und Aufgabenebene gewinnen weiterhin.",
|
||||||
"permission": "Berechtigung",
|
"permission": "Berechtigung",
|
||||||
"maxParallelExecutions": "Max. parallele Ausführungen",
|
"maxParallelExecutions": "Max. parallele Ausführungen",
|
||||||
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
||||||
|
"usageGateHeading": "Usage-Limit-Stopp",
|
||||||
|
"usageGateHint": "Ab dieser Auslastung holt die Queue keine neuen Tasks mehr; laufende Tasks laufen zu Ende. 0 schaltet den Stopp ab.",
|
||||||
|
"usageGateFiveHourPct": "5-Stunden-Fenster (%)",
|
||||||
|
"usageGateSevenDayPct": "7-Tage-Fenster (%)",
|
||||||
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
||||||
"standupWeekday": "Standup-Wochentag",
|
"standupWeekday": "Standup-Wochentag",
|
||||||
"weekdaySunday": "Sonntag",
|
"weekdaySunday": "Sonntag",
|
||||||
@@ -25,7 +33,13 @@
|
|||||||
"weekdayWednesday": "Mittwoch",
|
"weekdayWednesday": "Mittwoch",
|
||||||
"weekdayThursday": "Donnerstag",
|
"weekdayThursday": "Donnerstag",
|
||||||
"weekdayFriday": "Freitag",
|
"weekdayFriday": "Freitag",
|
||||||
"weekdaySaturday": "Samstag"
|
"weekdaySaturday": "Samstag",
|
||||||
|
"sessionSkills": "Session-Skills",
|
||||||
|
"sessionSkillsHint": "Gilt für jede Aufgabe. Kombiniert sich mit Listen- und Aufgaben-Auswahl.",
|
||||||
|
"accentPreset": "Akzentfarbe",
|
||||||
|
"accentPresetMoss": "Moos",
|
||||||
|
"accentPresetPeat": "Torf",
|
||||||
|
"accentPresetSea": "Meer"
|
||||||
},
|
},
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
"strategy": "Strategie",
|
"strategy": "Strategie",
|
||||||
@@ -95,11 +109,23 @@
|
|||||||
"systemPrompt": "System-Prompt (angehängt)",
|
"systemPrompt": "System-Prompt (angehängt)",
|
||||||
"promptPrepended": "Wird automatisch vorangestellt:",
|
"promptPrepended": "Wird automatisch vorangestellt:",
|
||||||
"agentFile": "Agent-Datei",
|
"agentFile": "Agent-Datei",
|
||||||
"browse": "Durchsuchen..."
|
"browse": "Durchsuchen...",
|
||||||
|
"sessionSkills": "Session-Skills",
|
||||||
|
"sessionSkillsHint": "Kombiniert sich additiv mit globaler (und Listen-)Auswahl."
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"installSection": "INSTALLIEREN",
|
||||||
|
"installUrlPlaceholder": "Git-Repo-URL (owner/repo oder vollständige URL)",
|
||||||
|
"installButton": "Installieren",
|
||||||
|
"installedSection": "INSTALLIERT",
|
||||||
|
"updateButton": "Aktualisieren",
|
||||||
|
"removeButton": "Entfernen",
|
||||||
|
"emptyState": "Keine Skills installiert — oben eine GitHub-URL einfügen, um einen hinzuzufügen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"showCompletedTip": "Abgeschlossene anzeigen",
|
"showCompletedTip": "Abgeschlossene anzeigen",
|
||||||
|
"letClaudeTip": "Claude machen lassen",
|
||||||
"listSettingsTip": "Listeneinstellungen",
|
"listSettingsTip": "Listeneinstellungen",
|
||||||
"addPlaceholder": "Aufgabe hinzufügen…",
|
"addPlaceholder": "Aufgabe hinzufügen…",
|
||||||
"enterKey": "ENTER",
|
"enterKey": "ENTER",
|
||||||
@@ -115,7 +141,7 @@
|
|||||||
"ctxMarkAs": "Markieren als",
|
"ctxMarkAs": "Markieren als",
|
||||||
"ctxMarkDone": "Erledigt",
|
"ctxMarkDone": "Erledigt",
|
||||||
"ctxMarkCancelled": "Abgebrochen",
|
"ctxMarkCancelled": "Abgebrochen",
|
||||||
"ctxRunInteractively": "Interaktiv ausführen",
|
"ctxOpenConPtySession": "ConPTY-Sitzung öffnen",
|
||||||
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
||||||
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
||||||
"ctxFinalizePlanningSession": "Plan finalisieren",
|
"ctxFinalizePlanningSession": "Plan finalisieren",
|
||||||
@@ -126,6 +152,10 @@
|
|||||||
"ctxAddToMyDay": "Zu Mein Tag hinzufügen",
|
"ctxAddToMyDay": "Zu Mein Tag hinzufügen",
|
||||||
"badgeDraft": "ENTWURF",
|
"badgeDraft": "ENTWURF",
|
||||||
"badgePlanned": "GEPLANT",
|
"badgePlanned": "GEPLANT",
|
||||||
|
"badgeManual": "MANUELL",
|
||||||
|
"manualTip": "Manuelle Erinnerung — Claude nimmt sie nie auf",
|
||||||
|
"ctxMarkManual": "Als manuell markieren",
|
||||||
|
"ctxMarkClaudeTask": "Als Claude-Aufgabe markieren",
|
||||||
"approve": "Genehmigen",
|
"approve": "Genehmigen",
|
||||||
"approveTip": "Genehmigen — als Erledigt markieren",
|
"approveTip": "Genehmigen — als Erledigt markieren",
|
||||||
"reject": "Ablehnen",
|
"reject": "Ablehnen",
|
||||||
@@ -137,6 +167,7 @@
|
|||||||
"removeFromQueueTip": "Aus Warteschlange entfernen",
|
"removeFromQueueTip": "Aus Warteschlange entfernen",
|
||||||
"toggleSubtasksTip": "Unteraufgaben ein-/ausklappen",
|
"toggleSubtasksTip": "Unteraufgaben ein-/ausklappen",
|
||||||
"agentSuggestedTip": "Vom Agenten vorgeschlagen",
|
"agentSuggestedTip": "Vom Agenten vorgeschlagen",
|
||||||
|
"interactiveChipTip": "Interaktive Sitzung offen — klicken, um im Mission Control dorthin zu springen",
|
||||||
"scheduleTitle": "Aufgabe planen",
|
"scheduleTitle": "Aufgabe planen",
|
||||||
"scheduleWhen": "WANN",
|
"scheduleWhen": "WANN",
|
||||||
"scheduleConfirm": "Planen",
|
"scheduleConfirm": "Planen",
|
||||||
@@ -145,7 +176,8 @@
|
|||||||
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
||||||
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
||||||
"rerun": "Erneut ausführen",
|
"rerun": "Erneut ausführen",
|
||||||
"refineTip": "Aufgabe mit Claude verfeinern"
|
"refineTip": "Aufgabe mit Claude verfeinern",
|
||||||
|
"refiningTip": "Claude verfeinert diese Aufgabe…"
|
||||||
},
|
},
|
||||||
"lists": {
|
"lists": {
|
||||||
"heading": "Listen",
|
"heading": "Listen",
|
||||||
@@ -158,6 +190,7 @@
|
|||||||
"contextWorktrees": "Worktrees…",
|
"contextWorktrees": "Worktrees…",
|
||||||
"contextOpenExplorer": "Im Explorer öffnen",
|
"contextOpenExplorer": "Im Explorer öffnen",
|
||||||
"contextOpenTerminal": "Im Terminal öffnen",
|
"contextOpenTerminal": "Im Terminal öffnen",
|
||||||
|
"contextLetClaude": "Claude machen lassen",
|
||||||
"newList": "Neue Liste",
|
"newList": "Neue Liste",
|
||||||
"addReposTip": "Repos als Listen hinzufügen"
|
"addReposTip": "Repos als Listen hinzufügen"
|
||||||
},
|
},
|
||||||
@@ -173,7 +206,7 @@
|
|||||||
"mergeTargetLabel": "Merge-Ziel",
|
"mergeTargetLabel": "Merge-Ziel",
|
||||||
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
|
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
|
||||||
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
|
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
|
||||||
"childOutcomesLabel": "VERBESSERUNGEN",
|
"childOutcomesLabel": "TEILAUFGABEN",
|
||||||
"stepsLabel": "SCHRITTE",
|
"stepsLabel": "SCHRITTE",
|
||||||
"addStepPlaceholder": "Schritt hinzufügen...",
|
"addStepPlaceholder": "Schritt hinzufügen...",
|
||||||
"detailsLabel": "DETAILS",
|
"detailsLabel": "DETAILS",
|
||||||
@@ -186,6 +219,12 @@
|
|||||||
"prepTitle": "Tagesvorbereitung",
|
"prepTitle": "Tagesvorbereitung",
|
||||||
"planDay": "Tag planen",
|
"planDay": "Tag planen",
|
||||||
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
||||||
|
"roadblockReply": {
|
||||||
|
"placeholder": "Auf den Roadblock antworten…",
|
||||||
|
"send": "Senden",
|
||||||
|
"noSession": "Keine Sitzung zum Fortsetzen — dieser Lauf hat keine Session-ID zum Antworten.",
|
||||||
|
"failed": "Antwort konnte nicht gesendet werden: {0}"
|
||||||
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"sectionLabel": "ANHÄNGE",
|
"sectionLabel": "ANHÄNGE",
|
||||||
"dropToAttach": "Zum Anhängen ablegen",
|
"dropToAttach": "Zum Anhängen ablegen",
|
||||||
@@ -194,6 +233,7 @@
|
|||||||
"addedSummary": "✓ Hinzugefügt: {0} ({1} Datei(en))",
|
"addedSummary": "✓ Hinzugefügt: {0} ({1} Datei(en))",
|
||||||
"overLimitError": "Konnte {0} nicht hinzufügen: {1}",
|
"overLimitError": "Konnte {0} nicht hinzufügen: {1}",
|
||||||
"invalidNameError": "Konnte {0} nicht hinzufügen: {1}",
|
"invalidNameError": "Konnte {0} nicht hinzufügen: {1}",
|
||||||
|
"dropReadError": "Abgelegte Dateien konnten nicht gelesen werden: {0}",
|
||||||
"selectIdleTask": "Zuerst eine inaktive Aufgabe auswählen"
|
"selectIdleTask": "Zuerst eine inaktive Aufgabe auswählen"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
@@ -231,27 +271,28 @@
|
|||||||
"chipDone": "FERTIG",
|
"chipDone": "FERTIG",
|
||||||
"chipFailed": "FEHLGESCHLAGEN",
|
"chipFailed": "FEHLGESCHLAGEN",
|
||||||
"reviewContinueTip": "Dieses Feedback senden und die Aufgabe erneut ausführen",
|
"reviewContinueTip": "Dieses Feedback senden und die Aufgabe erneut ausführen",
|
||||||
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen",
|
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
||||||
"composer": {
|
|
||||||
"placeholder": "Nachricht an die Sitzung…",
|
|
||||||
"send": "Senden",
|
|
||||||
"stop": "Sitzung beenden",
|
|
||||||
"interrupt": "Aktuellen Zug unterbrechen",
|
|
||||||
"queued": "Wartet — wird nach dem aktuellen Zug gesendet",
|
|
||||||
"unqueue": "Aus Warteschlange entfernen"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "In App öffnen",
|
|
||||||
"cancel": "Abbrechen",
|
|
||||||
"detach": "Abdocken",
|
|
||||||
"redock": "Andocken",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"clearFinished": "Erledigte entfernen",
|
"newSession": "Neue Sitzung",
|
||||||
"empty": "Keine laufenden Aufgaben",
|
"empty": "Keine laufenden Aufgaben",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"queue": "Warteschlange",
|
"queue": "Warteschlange",
|
||||||
"blocked": "Blockiert",
|
"blocked": "Blockiert",
|
||||||
|
"running": "Läuft",
|
||||||
|
"focusMode": "Fokus",
|
||||||
|
"overviewMode": "Übersicht",
|
||||||
|
"closeSession": "Sitzung schließen",
|
||||||
|
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
||||||
|
"conptyStarting": "Sitzung wird gestartet…",
|
||||||
|
"mergeHelperTitle": "Merge-Helfer",
|
||||||
|
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
||||||
|
"mergeHelperTaskDescriptionHeader": "Von diesem Lauf bearbeitete Tasks:",
|
||||||
|
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
||||||
|
"submitForReview": "Zum Review einreichen",
|
||||||
|
"submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann",
|
||||||
|
"planningTitleSuffix": " (Planung)",
|
||||||
"question": {
|
"question": {
|
||||||
"title": "Claude fragt nach",
|
"title": "Claude fragt nach",
|
||||||
"placeholder": "Antwort eingeben…",
|
"placeholder": "Antwort eingeben…",
|
||||||
@@ -292,8 +333,14 @@
|
|||||||
"workingDirectoryPlaceholder": "(keines)",
|
"workingDirectoryPlaceholder": "(keines)",
|
||||||
"browse": "Durchsuchen...",
|
"browse": "Durchsuchen...",
|
||||||
"defaultCommitType": "Standard-Commit-Typ",
|
"defaultCommitType": "Standard-Commit-Typ",
|
||||||
|
"manualList": "Manuelle Liste (Erinnerungen)",
|
||||||
|
"manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen"
|
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
||||||
|
"sectionVerify": "VERIFIKATION",
|
||||||
|
"verifyCommand": "Verifikationskommando",
|
||||||
|
"verifyCommandPlaceholder": "(keines — Merge verhält sich wie bisher)",
|
||||||
|
"verifyCommandHint": "Läuft im Arbeitsverzeichnis nach einem Merge, bevor die Aufgabe auf 'Erledigt' gesetzt wird. Ein Exit-Code ungleich 0 (oder Timeout) lässt den Merge bestehen, verhindert aber 'Erledigt' und meldet den Fehler."
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
"title": "WORKTREE MERGEN",
|
"title": "WORKTREE MERGEN",
|
||||||
@@ -347,6 +394,18 @@
|
|||||||
"forgetFolders": "Ordner vergessen",
|
"forgetFolders": "Ordner vergessen",
|
||||||
"alreadyAdded": "(bereits hinzugefügt)"
|
"alreadyAdded": "(bereits hinzugefügt)"
|
||||||
},
|
},
|
||||||
|
"mergeHelper": {
|
||||||
|
"title": "CLAUDE MACHEN LASSEN",
|
||||||
|
"windowTitle": "Claude machen lassen",
|
||||||
|
"scopeList": "Liste: {0}",
|
||||||
|
"confirm": "Claude machen lassen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"selectAll": "Alle auswählen",
|
||||||
|
"selectNone": "Keine auswählen",
|
||||||
|
"empty": "Keine Aufgaben zu übergeben — alles erledigt oder abgebrochen.",
|
||||||
|
"columnTask": "AUFGABE",
|
||||||
|
"columnStatus": "STATUS"
|
||||||
|
},
|
||||||
"unfinishedPlanning": {
|
"unfinishedPlanning": {
|
||||||
"title": "UNVOLLENDETE PLANUNGSSITZUNG",
|
"title": "UNVOLLENDETE PLANUNGSSITZUNG",
|
||||||
"windowTitle": "Unvollendete Planungssitzung",
|
"windowTitle": "Unvollendete Planungssitzung",
|
||||||
@@ -363,6 +422,39 @@
|
|||||||
"generate": "Erstellen",
|
"generate": "Erstellen",
|
||||||
"regenerate": "Neu erstellen",
|
"regenerate": "Neu erstellen",
|
||||||
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
|
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
|
||||||
|
},
|
||||||
|
"usageMonitor": {
|
||||||
|
"title": "USAGE MONITOR",
|
||||||
|
"windowTitle": "Usage Monitor",
|
||||||
|
"noGauges": "Noch keine Nutzungslimits gemeldet.",
|
||||||
|
"staleFormat": "Werte veraltet (Stand {0})",
|
||||||
|
"staleGateHint": "Das Gate greift in diesem Zustand nicht.",
|
||||||
|
"gateBlockedFormat": "Queue pausiert — {0}",
|
||||||
|
"resetIn": "Reset in {0}",
|
||||||
|
"gaugeSession": "Session (5 Std.)",
|
||||||
|
"gaugeWeeklyAll": "Woche (alle Modelle)",
|
||||||
|
"gaugeWeeklyScopedFormat": "Woche ({0})",
|
||||||
|
"period": "Zeitraum:",
|
||||||
|
"last7Days": "7 Tage",
|
||||||
|
"last30Days": "30 Tage",
|
||||||
|
"fromDate": "Von",
|
||||||
|
"toDate": "Bis",
|
||||||
|
"modelUsage": "Modelle",
|
||||||
|
"taskUsage": "Tasks",
|
||||||
|
"emptyStateHint": "Noch keine Nutzungsdaten für diesen Zeitraum.",
|
||||||
|
"columnModel": "MODELL",
|
||||||
|
"columnIn": "IN",
|
||||||
|
"columnOut": "OUT",
|
||||||
|
"columnCache": "CACHE",
|
||||||
|
"columnOtherIn": "ÜBRIGES IN",
|
||||||
|
"columnOtherOut": "ÜBRIGES OUT",
|
||||||
|
"columnOtherCache": "ÜBRIGES CACHE",
|
||||||
|
"columnShare": "ANTEIL",
|
||||||
|
"columnTask": "TASK",
|
||||||
|
"columnList": "LISTE",
|
||||||
|
"columnModelShort": "MODELL",
|
||||||
|
"columnRuns": "RUNS",
|
||||||
|
"columnTotal": "GESAMT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installer": {
|
"installer": {
|
||||||
@@ -476,6 +568,7 @@
|
|||||||
"restartWorker": "Worker neu starten",
|
"restartWorker": "Worker neu starten",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Wochenbericht…",
|
"weeklyReport": "Wochenbericht…",
|
||||||
|
"usageMonitor": "Usage Monitor…",
|
||||||
"about": "Über…",
|
"about": "Über…",
|
||||||
"addRepos": "Repos als Listen hinzufügen…"
|
"addRepos": "Repos als Listen hinzufügen…"
|
||||||
},
|
},
|
||||||
@@ -488,22 +581,41 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
||||||
"shell": { "restartingWorker": "Worker wird neu gestartet…" },
|
"shell": { "restartingWorker": "Worker wird neu gestartet…" },
|
||||||
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt" },
|
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" },
|
||||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "runInteractiveFailed": "Interaktiv ausführen fehlgeschlagen: {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}" },
|
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar." },
|
||||||
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
||||||
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." },
|
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien).", "buildFailed": "Kombinierte Vorschau konnte nicht erstellt werden: {0}" },
|
||||||
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||||
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
||||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
||||||
|
"usageMonitor": { "loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}" },
|
||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." },
|
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
||||||
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." },
|
||||||
"listSettings": { "untitled": "Unbenannt" },
|
"listSettings": { "untitled": "Unbenannt" },
|
||||||
|
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt." },
|
||||||
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"pill": {
|
||||||
|
"empty": "Nutzung –",
|
||||||
|
"bothFormat": "5h {0}% · 7d {1}%",
|
||||||
|
"fiveHourFormat": "5h {0}%",
|
||||||
|
"sevenDayFormat": "7d {0}%",
|
||||||
|
"fiveHourLabel": "5h",
|
||||||
|
"sevenDayLabel": "7d",
|
||||||
|
"resetIn": "{0}: Reset in {1}",
|
||||||
|
"durationHoursMinutes": "{0} h {1} m",
|
||||||
|
"durationMinutes": "{0} m",
|
||||||
|
"blockedReason": "Blockiert: {0}",
|
||||||
|
"stale": "veraltet (Stand {0})",
|
||||||
|
"lastError": "Letzter Fehler: {0}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,22 @@
|
|||||||
"tabWorktrees": "Worktrees",
|
"tabWorktrees": "Worktrees",
|
||||||
"tabFiles": "Files",
|
"tabFiles": "Files",
|
||||||
"tabPrime": "Prime Claude",
|
"tabPrime": "Prime Claude",
|
||||||
|
"tabSkills": "Skills",
|
||||||
"general": {
|
"general": {
|
||||||
"defaultInstructions": "Default instructions",
|
"defaultInstructions": "Default instructions",
|
||||||
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"maxTurns": "Max turns",
|
"maxTurns": "Max turns",
|
||||||
|
"effort": "Effort",
|
||||||
|
"modelPresets": "Per-model defaults",
|
||||||
|
"modelPresetsHint": "Reasoning effort and turn budget used when a session runs under that model. List- and task-level max-turn overrides still win.",
|
||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"maxParallelExecutions": "Max parallel executions",
|
"maxParallelExecutions": "Max parallel executions",
|
||||||
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
||||||
|
"usageGateHeading": "Usage limit stop",
|
||||||
|
"usageGateHint": "Once usage reaches this level, the queue stops picking up new tasks; running tasks finish normally. 0 turns the stop off.",
|
||||||
|
"usageGateFiveHourPct": "5-hour window (%)",
|
||||||
|
"usageGateSevenDayPct": "7-day window (%)",
|
||||||
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
||||||
"standupWeekday": "Standup weekday",
|
"standupWeekday": "Standup weekday",
|
||||||
"weekdaySunday": "Sunday",
|
"weekdaySunday": "Sunday",
|
||||||
@@ -25,7 +33,13 @@
|
|||||||
"weekdayWednesday": "Wednesday",
|
"weekdayWednesday": "Wednesday",
|
||||||
"weekdayThursday": "Thursday",
|
"weekdayThursday": "Thursday",
|
||||||
"weekdayFriday": "Friday",
|
"weekdayFriday": "Friday",
|
||||||
"weekdaySaturday": "Saturday"
|
"weekdaySaturday": "Saturday",
|
||||||
|
"sessionSkills": "Session skills",
|
||||||
|
"sessionSkillsHint": "Applied to every task. Combines with list- and task-level selections.",
|
||||||
|
"accentPreset": "Accent color",
|
||||||
|
"accentPresetMoss": "Moss",
|
||||||
|
"accentPresetPeat": "Peat",
|
||||||
|
"accentPresetSea": "Sea"
|
||||||
},
|
},
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
"strategy": "Strategy",
|
"strategy": "Strategy",
|
||||||
@@ -95,11 +109,23 @@
|
|||||||
"systemPrompt": "System prompt (appended)",
|
"systemPrompt": "System prompt (appended)",
|
||||||
"promptPrepended": "Prepended automatically:",
|
"promptPrepended": "Prepended automatically:",
|
||||||
"agentFile": "Agent file",
|
"agentFile": "Agent file",
|
||||||
"browse": "Browse..."
|
"browse": "Browse...",
|
||||||
|
"sessionSkills": "Session skills",
|
||||||
|
"sessionSkillsHint": "Combines additively with global (and list) selections."
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"installSection": "INSTALL",
|
||||||
|
"installUrlPlaceholder": "Git repo URL (owner/repo or full URL)",
|
||||||
|
"installButton": "Install",
|
||||||
|
"installedSection": "INSTALLED",
|
||||||
|
"updateButton": "Update",
|
||||||
|
"removeButton": "Remove",
|
||||||
|
"emptyState": "No skills installed — paste a GitHub URL above to add one."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"showCompletedTip": "Show completed",
|
"showCompletedTip": "Show completed",
|
||||||
|
"letClaudeTip": "Let Claude handle it",
|
||||||
"listSettingsTip": "List settings",
|
"listSettingsTip": "List settings",
|
||||||
"addPlaceholder": "Add a task…",
|
"addPlaceholder": "Add a task…",
|
||||||
"enterKey": "ENTER",
|
"enterKey": "ENTER",
|
||||||
@@ -115,7 +141,7 @@
|
|||||||
"ctxMarkAs": "Mark as",
|
"ctxMarkAs": "Mark as",
|
||||||
"ctxMarkDone": "Done",
|
"ctxMarkDone": "Done",
|
||||||
"ctxMarkCancelled": "Cancelled",
|
"ctxMarkCancelled": "Cancelled",
|
||||||
"ctxRunInteractively": "Run interactively",
|
"ctxOpenConPtySession": "Open ConPTY session",
|
||||||
"ctxOpenPlanningSession": "Open planning Session",
|
"ctxOpenPlanningSession": "Open planning Session",
|
||||||
"ctxResumePlanningSession": "Resume planning Session",
|
"ctxResumePlanningSession": "Resume planning Session",
|
||||||
"ctxFinalizePlanningSession": "Finalize plan",
|
"ctxFinalizePlanningSession": "Finalize plan",
|
||||||
@@ -126,6 +152,10 @@
|
|||||||
"ctxAddToMyDay": "Add to My Day",
|
"ctxAddToMyDay": "Add to My Day",
|
||||||
"badgeDraft": "DRAFT",
|
"badgeDraft": "DRAFT",
|
||||||
"badgePlanned": "PLANNED",
|
"badgePlanned": "PLANNED",
|
||||||
|
"badgeManual": "MANUAL",
|
||||||
|
"manualTip": "Manual reminder — Claude never picks this up",
|
||||||
|
"ctxMarkManual": "Mark as manual",
|
||||||
|
"ctxMarkClaudeTask": "Mark as Claude task",
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
"approveTip": "Approve — mark Done",
|
"approveTip": "Approve — mark Done",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
@@ -137,6 +167,7 @@
|
|||||||
"removeFromQueueTip": "Remove from queue",
|
"removeFromQueueTip": "Remove from queue",
|
||||||
"toggleSubtasksTip": "Expand / collapse subtasks",
|
"toggleSubtasksTip": "Expand / collapse subtasks",
|
||||||
"agentSuggestedTip": "Suggested by the agent",
|
"agentSuggestedTip": "Suggested by the agent",
|
||||||
|
"interactiveChipTip": "Interactive session open — click to jump to it in Mission Control",
|
||||||
"scheduleTitle": "Schedule task",
|
"scheduleTitle": "Schedule task",
|
||||||
"scheduleWhen": "WHEN",
|
"scheduleWhen": "WHEN",
|
||||||
"scheduleConfirm": "Schedule",
|
"scheduleConfirm": "Schedule",
|
||||||
@@ -145,7 +176,8 @@
|
|||||||
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
||||||
"feedbackPlaceholder": "What should the agent fix?",
|
"feedbackPlaceholder": "What should the agent fix?",
|
||||||
"rerun": "Re-run",
|
"rerun": "Re-run",
|
||||||
"refineTip": "Refine this task with Claude"
|
"refineTip": "Refine this task with Claude",
|
||||||
|
"refiningTip": "Claude is refining this task…"
|
||||||
},
|
},
|
||||||
"lists": {
|
"lists": {
|
||||||
"heading": "Lists",
|
"heading": "Lists",
|
||||||
@@ -158,6 +190,7 @@
|
|||||||
"contextWorktrees": "Worktrees…",
|
"contextWorktrees": "Worktrees…",
|
||||||
"contextOpenExplorer": "Open in Explorer",
|
"contextOpenExplorer": "Open in Explorer",
|
||||||
"contextOpenTerminal": "Open in Terminal",
|
"contextOpenTerminal": "Open in Terminal",
|
||||||
|
"contextLetClaude": "Let Claude handle it",
|
||||||
"newList": "New list",
|
"newList": "New list",
|
||||||
"addReposTip": "Add repos as lists"
|
"addReposTip": "Add repos as lists"
|
||||||
},
|
},
|
||||||
@@ -173,7 +206,7 @@
|
|||||||
"mergeTargetLabel": "Merge target",
|
"mergeTargetLabel": "Merge target",
|
||||||
"reviewCombinedDiff": "Review combined diff",
|
"reviewCombinedDiff": "Review combined diff",
|
||||||
"mergeAllSubtasks": "Merge all subtasks",
|
"mergeAllSubtasks": "Merge all subtasks",
|
||||||
"childOutcomesLabel": "IMPROVEMENTS",
|
"childOutcomesLabel": "SUBTASKS",
|
||||||
"stepsLabel": "STEPS",
|
"stepsLabel": "STEPS",
|
||||||
"addStepPlaceholder": "Add a step...",
|
"addStepPlaceholder": "Add a step...",
|
||||||
"detailsLabel": "DETAILS",
|
"detailsLabel": "DETAILS",
|
||||||
@@ -186,6 +219,12 @@
|
|||||||
"prepTitle": "Daily prep",
|
"prepTitle": "Daily prep",
|
||||||
"planDay": "Plan day",
|
"planDay": "Plan day",
|
||||||
"prepEmpty": "No prep run today yet — click Plan day",
|
"prepEmpty": "No prep run today yet — click Plan day",
|
||||||
|
"roadblockReply": {
|
||||||
|
"placeholder": "Reply to the roadblock…",
|
||||||
|
"send": "Send",
|
||||||
|
"noSession": "No session to resume — this run has no session ID to reply into.",
|
||||||
|
"failed": "Couldn't send the reply: {0}"
|
||||||
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"sectionLabel": "ATTACHMENTS",
|
"sectionLabel": "ATTACHMENTS",
|
||||||
"dropToAttach": "Drop to attach",
|
"dropToAttach": "Drop to attach",
|
||||||
@@ -194,6 +233,7 @@
|
|||||||
"addedSummary": "✓ Added {0} ({1} file(s))",
|
"addedSummary": "✓ Added {0} ({1} file(s))",
|
||||||
"overLimitError": "Could not add {0}: {1}",
|
"overLimitError": "Could not add {0}: {1}",
|
||||||
"invalidNameError": "Could not add {0}: {1}",
|
"invalidNameError": "Could not add {0}: {1}",
|
||||||
|
"dropReadError": "Couldn't read the dropped files: {0}",
|
||||||
"selectIdleTask": "Select an idle task first"
|
"selectIdleTask": "Select an idle task first"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
@@ -231,27 +271,28 @@
|
|||||||
"chipDone": "DONE",
|
"chipDone": "DONE",
|
||||||
"chipFailed": "FAILED",
|
"chipFailed": "FAILED",
|
||||||
"reviewContinueTip": "Send this feedback and re-run the task",
|
"reviewContinueTip": "Send this feedback and re-run the task",
|
||||||
"reviewResetTip": "Discard all changes and reset the task to Idle",
|
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
||||||
"composer": {
|
|
||||||
"placeholder": "Message the session…",
|
|
||||||
"send": "Send",
|
|
||||||
"stop": "Stop session",
|
|
||||||
"interrupt": "Interrupt current turn",
|
|
||||||
"queued": "Queued — sends after the current turn",
|
|
||||||
"unqueue": "Remove from queue"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "Open in app",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"detach": "Detach",
|
|
||||||
"redock": "Re-dock",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"clearFinished": "Clear finished",
|
"newSession": "New session",
|
||||||
"empty": "No running tasks",
|
"empty": "No running tasks",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
|
"running": "Running",
|
||||||
|
"focusMode": "Focus",
|
||||||
|
"overviewMode": "Overview",
|
||||||
|
"closeSession": "Close session",
|
||||||
|
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
||||||
|
"conptyStarting": "Starting session…",
|
||||||
|
"mergeHelperTitle": "Merge Helper",
|
||||||
|
"mergeHelperTaskTitle": "List handler: {0}",
|
||||||
|
"mergeHelperTaskDescriptionHeader": "Tasks handled by this run:",
|
||||||
|
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
||||||
|
"submitForReview": "Submit for review",
|
||||||
|
"submitForReviewTip": "Commit this worktree and move the task to review so it can be merged",
|
||||||
|
"planningTitleSuffix": " (Planning)",
|
||||||
"question": {
|
"question": {
|
||||||
"title": "Claude is asking",
|
"title": "Claude is asking",
|
||||||
"placeholder": "Type your answer…",
|
"placeholder": "Type your answer…",
|
||||||
@@ -292,8 +333,14 @@
|
|||||||
"workingDirectoryPlaceholder": "(none)",
|
"workingDirectoryPlaceholder": "(none)",
|
||||||
"browse": "Browse...",
|
"browse": "Browse...",
|
||||||
"defaultCommitType": "Default commit type",
|
"defaultCommitType": "Default commit type",
|
||||||
|
"manualList": "Manual list (reminders)",
|
||||||
|
"manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Reset agent settings"
|
"resetAgentSettings": "Reset agent settings",
|
||||||
|
"sectionVerify": "VERIFICATION",
|
||||||
|
"verifyCommand": "Verify command",
|
||||||
|
"verifyCommandPlaceholder": "(none — merge behaves as today)",
|
||||||
|
"verifyCommandHint": "Runs in the working directory after a merge lands, before the task is marked Done. A non-zero exit (or timeout) leaves the merge in place but keeps the task out of Done and reports the failure."
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
"title": "MERGE WORKTREE",
|
"title": "MERGE WORKTREE",
|
||||||
@@ -347,6 +394,18 @@
|
|||||||
"forgetFolders": "Forget folders",
|
"forgetFolders": "Forget folders",
|
||||||
"alreadyAdded": "(already added)"
|
"alreadyAdded": "(already added)"
|
||||||
},
|
},
|
||||||
|
"mergeHelper": {
|
||||||
|
"title": "LET CLAUDE HANDLE IT",
|
||||||
|
"windowTitle": "Let Claude handle it",
|
||||||
|
"scopeList": "List: {0}",
|
||||||
|
"confirm": "Let Claude handle it",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"selectAll": "Select all",
|
||||||
|
"selectNone": "Select none",
|
||||||
|
"empty": "No tasks to hand over — everything is done or cancelled.",
|
||||||
|
"columnTask": "TASK",
|
||||||
|
"columnStatus": "STATUS"
|
||||||
|
},
|
||||||
"unfinishedPlanning": {
|
"unfinishedPlanning": {
|
||||||
"title": "UNFINISHED PLANNING SESSION",
|
"title": "UNFINISHED PLANNING SESSION",
|
||||||
"windowTitle": "Unfinished planning session",
|
"windowTitle": "Unfinished planning session",
|
||||||
@@ -363,6 +422,39 @@
|
|||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"emptyStateHint": "No report for this range yet. Click “Generate”."
|
"emptyStateHint": "No report for this range yet. Click “Generate”."
|
||||||
|
},
|
||||||
|
"usageMonitor": {
|
||||||
|
"title": "USAGE MONITOR",
|
||||||
|
"windowTitle": "Usage Monitor",
|
||||||
|
"noGauges": "No usage limits reported yet.",
|
||||||
|
"staleFormat": "Values stale (as of {0})",
|
||||||
|
"staleGateHint": "The gate does not apply while values are stale.",
|
||||||
|
"gateBlockedFormat": "Queue paused — {0}",
|
||||||
|
"resetIn": "Reset in {0}",
|
||||||
|
"gaugeSession": "Session (5h)",
|
||||||
|
"gaugeWeeklyAll": "Week (all models)",
|
||||||
|
"gaugeWeeklyScopedFormat": "Week ({0})",
|
||||||
|
"period": "Period:",
|
||||||
|
"last7Days": "7 days",
|
||||||
|
"last30Days": "30 days",
|
||||||
|
"fromDate": "From",
|
||||||
|
"toDate": "To",
|
||||||
|
"modelUsage": "Models",
|
||||||
|
"taskUsage": "Tasks",
|
||||||
|
"emptyStateHint": "No usage data for this range yet.",
|
||||||
|
"columnModel": "MODEL",
|
||||||
|
"columnIn": "IN",
|
||||||
|
"columnOut": "OUT",
|
||||||
|
"columnCache": "CACHE",
|
||||||
|
"columnOtherIn": "OTHER IN",
|
||||||
|
"columnOtherOut": "OTHER OUT",
|
||||||
|
"columnOtherCache": "OTHER CACHE",
|
||||||
|
"columnShare": "SHARE",
|
||||||
|
"columnTask": "TASK",
|
||||||
|
"columnList": "LIST",
|
||||||
|
"columnModelShort": "MODEL",
|
||||||
|
"columnRuns": "RUNS",
|
||||||
|
"columnTotal": "TOTAL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installer": {
|
"installer": {
|
||||||
@@ -476,6 +568,7 @@
|
|||||||
"restartWorker": "Restart worker",
|
"restartWorker": "Restart worker",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Weekly Report…",
|
"weeklyReport": "Weekly Report…",
|
||||||
|
"usageMonitor": "Usage Monitor…",
|
||||||
"about": "About…",
|
"about": "About…",
|
||||||
"addRepos": "Add repos as lists…"
|
"addRepos": "Add repos as lists…"
|
||||||
},
|
},
|
||||||
@@ -488,22 +581,41 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
||||||
"shell": { "restartingWorker": "Restarting worker…" },
|
"shell": { "restartingWorker": "Restarting worker…" },
|
||||||
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||||
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked" },
|
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" },
|
||||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "runInteractiveFailed": "Run interactively failed: {0}", "planningOpenFailed": "Couldn't open planning session: {0}" },
|
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available." },
|
||||||
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
||||||
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." },
|
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files).", "buildFailed": "Could not build combined preview: {0}" },
|
||||||
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||||
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
||||||
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
||||||
|
"usageMonitor": { "loadFailed": "Couldn't load usage data: {0}" },
|
||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." },
|
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
||||||
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." },
|
||||||
"listSettings": { "untitled": "Untitled" },
|
"listSettings": { "untitled": "Untitled" },
|
||||||
|
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done." },
|
||||||
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"pill": {
|
||||||
|
"empty": "Usage –",
|
||||||
|
"bothFormat": "5h {0}% · 7d {1}%",
|
||||||
|
"fiveHourFormat": "5h {0}%",
|
||||||
|
"sevenDayFormat": "7d {0}%",
|
||||||
|
"fiveHourLabel": "5h",
|
||||||
|
"sevenDayLabel": "7d",
|
||||||
|
"resetIn": "{0}: Reset in {1}",
|
||||||
|
"durationHoursMinutes": "{0} h {1} m",
|
||||||
|
"durationMinutes": "{0} m",
|
||||||
|
"blockedReason": "Blocked: {0}",
|
||||||
|
"stale": "stale (as of {0})",
|
||||||
|
"lastError": "Last error: {0}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui;
|
||||||
|
|
||||||
|
public sealed record AccentPreset(string Name, string Accent, string Dim, string Soft, string Glow);
|
||||||
|
|
||||||
|
public static class AccentPresets
|
||||||
|
{
|
||||||
|
// Hue 88 — moss/sage green (original)
|
||||||
|
public static readonly AccentPreset Moss = new("moss", "#FF7C9166", "#FF64785A", "#FF3E4B39", "#387C9166");
|
||||||
|
// Hue ~40 — warm earthy brown/terra
|
||||||
|
public static readonly AccentPreset Peat = new("peat", "#FF9A7B5C", "#FF7F6449", "#FF4D3C2C", "#389A7B5C");
|
||||||
|
// Hue ~180 — cool teal/sea-green
|
||||||
|
public static readonly AccentPreset Sea = new("sea", "#FF5B8F8C", "#FF4A7573", "#FF263D3C", "#385B8F8C");
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<AccentPreset> All = [Moss, Peat, Sea];
|
||||||
|
public static AccentPreset Default => Moss;
|
||||||
|
|
||||||
|
public static AccentPreset Find(string? name) =>
|
||||||
|
All.FirstOrDefault(p => p.Name == name) ?? Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class AccentPresetSwatchViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public AccentPreset Preset { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
|
||||||
|
public Color DisplayColor { get; }
|
||||||
|
|
||||||
|
public string DisplayName =>
|
||||||
|
Loc.T($"settings.general.accentPreset{char.ToUpperInvariant(Preset.Name[0])}{Preset.Name.Substring(1)}");
|
||||||
|
|
||||||
|
public AccentPresetSwatchViewModel(AccentPreset preset, bool selected)
|
||||||
|
{
|
||||||
|
Preset = preset;
|
||||||
|
_isSelected = selected;
|
||||||
|
DisplayColor = Color.Parse(preset.Accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ public sealed class AppSettings
|
|||||||
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
||||||
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
|
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
|
||||||
public string Language { get; set; } = "";
|
public string Language { get; set; } = "";
|
||||||
|
public string AccentPreset { get; set; } = "";
|
||||||
|
|
||||||
private static readonly string ConfigPath = Paths.Expand("~/.todo-app/ui.config.json");
|
private static readonly string ConfigPath = Paths.Expand("~/.todo-app/ui.config.json");
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ ViewModels/
|
|||||||
Islands/ — ListsIsland, TasksIsland, DetailsIsland, TaskRow, ListNavItem,
|
Islands/ — ListsIsland, TasksIsland, DetailsIsland, TaskRow, ListNavItem,
|
||||||
NotesEditor, MergePreviewPresenter
|
NotesEditor, MergePreviewPresenter
|
||||||
Agent/ — AgentConfigEditorViewModel (scope-parameterized: List | Task)
|
Agent/ — AgentConfigEditorViewModel (scope-parameterized: List | Task)
|
||||||
Modals/ — About, DiffViewer (+ DiffModels), ListSettings, Merge, RepoImport,
|
Modals/ — About, DiffViewer (+ DiffModels), ListSettings, Merge, MergeHelperSelection,
|
||||||
Settings (+ Settings/ tab VMs), UnfinishedPlanning, WeeklyReport,
|
RepoImport, Settings (+ Settings/ tab VMs), UnfinishedPlanning, WeeklyReport,
|
||||||
WorkerConnection, WorktreesOverview, UnifiedDiffParser
|
WorkerConnection, WorktreesOverview, UnifiedDiffParser
|
||||||
Conflicts/ — ConflictResolverViewModel + ConflictModels (MergeFile/MergeFileSegment/MergeConflictBlock)
|
Conflicts/ — ConflictResolverViewModel + ConflictModels (MergeFile/MergeFileSegment/MergeConflictBlock)
|
||||||
Views/ — mirrors the VM layout; Islands/Detail/ holds TaskHeaderBar,
|
Views/ — mirrors the VM layout; Islands/Detail/ holds TaskHeaderBar,
|
||||||
@@ -35,17 +35,18 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
|
|||||||
|
|
||||||
- **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`.
|
- **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`.
|
||||||
- **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`.
|
- **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`.
|
||||||
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RunInteractivelyAsync`, `RefineTask`; MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell.
|
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RefineTask`, `OpenConPtySessionRequested` (embedded ConPTY terminal), `ToggleManual` (per-task manual flag) and `SyncInteractiveSessions` (mirrors Mission Control's open ConPTY panes onto the rows); MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell.
|
||||||
- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards), the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel` and call `ShowDiffViewer`), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection<AttachmentRowViewModel>`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file.
|
- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards) — the ROADBLOCK card also has a reply field (`RoadblockReplyDraft`/`SendRoadblockReplyCommand`, gated by `CanReplyToRoadblock` on `LatestRunSessionId`) that resumes the session via the same `ContinueTaskAsync` transport as `ContinueCommand` but with the user's own text instead of the fixed re-run prompt; failures raise `ErrorReported`, wired by the shell into `FlashFooterError`, the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, call `ShowDiffViewer`, and fire the `DiffViewed` callback; `HasReviewableDiff` reports whether anything is inspectable, feeding the review gate), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection<AttachmentRowViewModel>`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file.
|
||||||
- **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; list row: kind Smart/Virtual/User, count, icon/dot keys, drop hints).
|
- **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; plus `IsManual` (→ MANUAL badge; suppresses `CanSendToQueue`/`CanRefine`/`CanOpenPlanningSession`) and `HasInteractiveSession` (→ accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane); list row: kind Smart/Virtual/User, count, icon/dot keys, drop hints, `IsManual`).
|
||||||
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
||||||
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows), `ListSettingsModalViewModel` (name, working dir, commit type, delete list; hosts shared `AgentConfigEditorViewModel` as `Agent` property (scope=List) — save delegates to `Agent.SaveAsync()`), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`, `LogVisualizerViewModel` (worker logs, last 30 min, all levels + a warn/error-only filter; loads via `GetRecentLogsAsync`).
|
- **UsagePillViewModel** — one shared instance backs the `UsagePill` control hosted in both the footer and the Mission Control header; loads via `GetUsageSnapshotAsync` and updates live off `IWorkerClient.UsageUpdatedEvent`; derives display text, tooltip, and dot state (normal/warn/stale/blocked, mutually exclusive priority blocked > stale > warn > normal) from the shared `UsageSnapshotDto`.
|
||||||
|
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows). General hosts the per-model preset table (`ModelPresets` → `ModelPresetRowViewModel`: effort + max turns per alias) which **replaced** the single global "Max turns" field, `ListSettingsModalViewModel` (name, working dir, commit type, "manual list" flag, `VerifyCommand` (optional post-merge verify gate, own field/section — not part of `AgentConfigEditorViewModel`), delete list; hosts shared `AgentConfigEditorViewModel` as `Agent` property (scope=List) — save delegates to `Agent.SaveAsync(verifyCommand)`, since both fields land in the same `list_config` row via one `UpdateListConfig` call and would otherwise clobber each other), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `MergeHelperSelectionModalViewModel` ("Let Claude handle it": checkbox picker over one list's non-terminal, non-manual tasks, pre-ticks the actionable ones; list-scoped only — `Configure(listId, listName)`, no global scope. Opened from the list row's context menu, which is hidden when the list has no working dir; on confirm `ListsIslandViewModel` raises `LetClaudeHandleRequested` → shell → `MissionControlViewModel.OpenMergeHelperConPtySessionAsync`, which first calls `IWorkerClient.CreateMergeHelperTaskAsync` to create one new ClaudeDo task per run in that list — `Idle`/`IsManual=true` (never queued), title/description localized (`missionControl.mergeHelperTaskTitle`/`mergeHelperTaskDescriptionHeader`), `TaskEntity.HandlerBaseCommit` stamped to the list repo's current HEAD — then opens a **task-based** ConPTY tile for it (deduped by `TaskId` like `OpenConPtySessionAsync`, not `CreateAdHoc`) running the five-phase handler prompt. The handler still merges the tasks it handles itself; the host task never gets a worktree of its own, so "Submit for review" stamps `HandlerHeadCommit` instead of committing a worktree, and the detail pane's `MergeSectionViewModel` falls back to `HandlerBaseCommit`/`HandlerHeadCommit` over the list's working dir for its diff — see `TaskEntity` in `ClaudeDo.Data/CLAUDE.md`), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`, `LogVisualizerViewModel` (worker logs, last 30 min, all levels + a warn/error-only filter; loads via `GetRecentLogsAsync`), `UsageMonitorModalViewModel` (opened from the usage pill; renders one gauge per row in `UsageSnapshotDto.Limits` — **dynamic**, since the fixed `seven_day_opus`/`seven_day_sonnet`-style buckets the raw Anthropic API can return are plan-dependent and come back `null` on plans that don't have them, so a fixed gauge layout would break; also shows model usage (`GetModelUsageAsync`, ClaudeDo-vs-Other split per model) and top-task usage (`GetTaskUsageAsync`) tables over a 7d/30d preset or custom date range).
|
||||||
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModels.cs` holds shared types: `DiffLineViewModel`, `DiffFileViewModel`, `DiffLineKind`, `DiffFileStatus`, `SubtaskDiffRow`, `DiffTreeNodeViewModel`, `DiffTree`. `DiffViewerViewModel` is a single unified read-only diff viewer with two modes: **Files** (dirty worktree / branch-vs-base / commit-range — loads via GitService, shows a folder file-tree on the left + per-file diff pane on the right, Merge button for live branch source) and **Planning** (per-subtask diffs via `GetPlanningAggregateAsync`, subtask list left + flat diff right, combined integration-branch toggle). The Merge button opens the merge form, which routes to `ConflictResolverViewModel` on conflict. `DiffLinesView` renders per-file diff content with binary/empty placeholders.
|
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModels.cs` holds shared types: `DiffLineViewModel`, `DiffFileViewModel`, `DiffLineKind`, `DiffFileStatus`, `SubtaskDiffRow`, `DiffTreeNodeViewModel`, `DiffTree`. `DiffViewerViewModel` is a single unified read-only diff viewer with two modes: **Files** (dirty worktree / branch-vs-base / commit-range — loads via GitService, shows a folder file-tree on the left + per-file diff pane on the right, Merge button for live branch source) and **Planning** (per-subtask diffs via `GetPlanningAggregateAsync`, subtask list left + flat diff right, combined integration-branch toggle). The Merge button opens the merge form, which routes to `ConflictResolverViewModel` on conflict. `DiffLinesView` renders per-file diff content with binary/empty placeholders.
|
||||||
- **Conflicts** — `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). The editor is reached from review **Approve** on conflict and from the **Merge** button in the Diff window (a conflicting `MergeTask` hands off to the resolver via `RequestConflictResolution`).
|
- **Conflicts** — `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). The editor is reached from review **Approve** on conflict and from the **Merge** button in the Diff window (a conflicting `MergeTask` hands off to the resolver via `RequestConflictResolution`).
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, interactive terminal, refine), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules, recent worker logs (`GetRecentLogsAsync`). Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
|
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, refine), embedded ConPTY launch specs (`GetInteractiveLaunchSpecAsync`/`GetAdHocLaunchSpecAsync`), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules, recent worker logs (`GetRecentLogsAsync`), usage monitoring (`GetUsageSnapshotAsync -> UsageSnapshotDto?`, `GetModelUsageAsync(from, to)`, `GetTaskUsageAsync(from, to)`). Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log, `UsageUpdatedEvent`). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
|
||||||
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||||
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
||||||
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
||||||
@@ -65,5 +66,9 @@ Modals use `TaskCompletionSource` results behind the reusable `ModalShell` contr
|
|||||||
- Context menus exist on both list rows and task rows; right-click selects before opening the menu
|
- Context menus exist on both list rows and task rows; right-click selects before opening the menu
|
||||||
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
||||||
- Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible.
|
- Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible.
|
||||||
|
- Window key bindings live on `MainWindow`: `Ctrl+K` focuses search, `Ctrl+N` the add-task box. Do **not** bind bare punctuation gestures — `OemQuestion` used to hold search focus and silently swallowed `#` app-wide on a German layout.
|
||||||
|
- `FocusClearing` also clears focus from a TextBox on Escape, mirroring its click-outside behavior — but the KeyDown handler is scoped to `MainWindow` specifically (`AddClassHandler<MainWindow>`, not `<TopLevel>`). Modal windows (`AboutModalView` etc.) each bind their own `Window.KeyBindings` Escape → close; since modals are separate `Window` instances, this handler never runs there, so Escape still closes them unchanged. Mission Control's ConPTY tiles (`InteractiveTerminalView`) live in `MissionControlWindow`, also unaffected — Escape always reaches the PTY there.
|
||||||
|
- `Ellipse.spinner` (IslandStyles) is the shared indeterminate spinner: used for a starting ConPTY pane (`InteractiveTerminalViewModel.IsStarting`) and in place of the refine button while `TaskRowViewModel.IsRefining`.
|
||||||
|
- `ConPtyPaneViewModel` resolves its own launch spec (ctor takes a descriptor **factory**; the host wires handlers and then calls `Start()`), so the Mission Control tile appears immediately with its spinner while the worker is still preparing the worktree. A failed launch keeps the tile with its inline error banner instead of never appearing.
|
||||||
- `SessionTerminalView` is the reusable log terminal (StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`) used for both the task `Log` and the prep `PrepLog`.
|
- `SessionTerminalView` is the reusable log terminal (StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`) used for both the task `Log` and the prep `PrepLog`.
|
||||||
- `DetailsIslandView` is a pane-wide drag-and-drop file target (`DragDrop.AllowDrop`, Avalonia 12 `DataFormat.File`) with a "Drop to attach" hover overlay. `DescriptionStepsCard` shows an Attachments list (file name, size, remove button), an "Add file…" picker, and an explicit `DropStatus` confirmation line. Keys use the `details.attachments.*` localization namespace (en + de).
|
- `DetailsIslandView` is a pane-wide drag-and-drop file target (`DragDrop.AllowDrop`, Avalonia 12 `DataFormat.File`) with a "Drop to attach" hover overlay. `DescriptionStepsCard` shows an Attachments list (file name, size, remove button), an "Add file…" picker, and an explicit `DropStatus` confirmation line. Keys use the `details.attachments.*` localization namespace (en + de).
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
||||||
|
<PackageReference Include="Iciclecreek.Avalonia.Terminal" Version="2.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -57,8 +57,8 @@
|
|||||||
<!-- Icon.Search -->
|
<!-- Icon.Search -->
|
||||||
<StreamGeometry x:Key="Icon.Search">M11 4a7 7 0 1 0 0 14A7 7 0 0 0 11 4z M20 20l-3.5-3.5</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Search">M11 4a7 7 0 1 0 0 14A7 7 0 0 0 11 4z M20 20l-3.5-3.5</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.Plus -->
|
<!-- Icon.Plus — filled cross (PathIcon fills, so a stroke-only plus renders invisible) -->
|
||||||
<StreamGeometry x:Key="Icon.Plus">M12 5v14M5 12h14</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Plus">M10.6 5 H13.4 V10.6 H19 V13.4 H13.4 V19 H10.6 V13.4 H5 V10.6 H10.6 Z</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.MoreHorizontal (three filled dots) — uses fill so rendered via PathIcon with fill brush -->
|
<!-- Icon.MoreHorizontal (three filled dots) — uses fill so rendered via PathIcon with fill brush -->
|
||||||
<StreamGeometry x:Key="Icon.MoreHorizontal">M5 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M12 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M19 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0</StreamGeometry>
|
<StreamGeometry x:Key="Icon.MoreHorizontal">M5 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M12 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0 M19 12m-1.3 0a1.3 1.3 0 1 0 2.6 0 1.3 1.3 0 1 0-2.6 0</StreamGeometry>
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
|
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
|
||||||
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
|
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
|
||||||
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
|
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
|
||||||
|
<SolidColorBrush x:Key="ManualBadgeBrush" Color="{StaticResource TextFaintColor}"/>
|
||||||
|
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
|
|
||||||
@@ -247,6 +248,21 @@
|
|||||||
<Setter Property="Foreground" Value="#8FB9D6" />
|
<Setter Property="Foreground" Value="#8FB9D6" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- interactive → accent, the "live" family shared with running: the task has a ConPTY session
|
||||||
|
open in Mission Control. Clickable (jumps to that pane), hence the hand cursor + hover lift.
|
||||||
|
Deliberately NOT the slate-blue parked treatment — telling the two apart is the whole point. -->
|
||||||
|
<Style Selector="Border.chip.interactive">
|
||||||
|
<Setter Property="Background" Value="{StaticResource RunningTintBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.chip.interactive > TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.chip.interactive:pointerover">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Worktree-state chips (worktrees overview) -->
|
<!-- Worktree-state chips (worktrees overview) -->
|
||||||
<!-- active → slate-blue (same hue as parked: a live worktree) -->
|
<!-- active → slate-blue (same hue as parked: a live worktree) -->
|
||||||
<Style Selector="Border.chip.wt-active">
|
<Style Selector="Border.chip.wt-active">
|
||||||
@@ -454,6 +470,25 @@
|
|||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Indeterminate ring spinner (dashed ring, rotated forever) -->
|
||||||
|
<Style Selector="Ellipse.spinner">
|
||||||
|
<Setter Property="Width" Value="14" />
|
||||||
|
<Setter Property="Height" Value="14" />
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="2" />
|
||||||
|
<Setter Property="StrokeDashArray" Value="3,2" />
|
||||||
|
<Setter Property="RenderTransform" Value="rotate(0deg)" />
|
||||||
|
<Style.Animations>
|
||||||
|
<Animation Duration="0:0:0.9" IterationCount="INFINITE" Easing="LinearEasing">
|
||||||
|
<KeyFrame Cue="0%"> <Setter Property="RotateTransform.Angle" Value="0" /> </KeyFrame>
|
||||||
|
<KeyFrame Cue="100%"><Setter Property="RotateTransform.Angle" Value="360" /></KeyFrame>
|
||||||
|
</Animation>
|
||||||
|
</Style.Animations>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Ellipse.spinner.dim">
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource TextDimBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- AGENT STRIP -->
|
<!-- AGENT STRIP -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
@@ -658,6 +693,10 @@
|
|||||||
<Setter Property="CornerRadius" Value="8" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
<Setter Property="Cursor" Value="Hand" />
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<!-- Reserved so the task-drop highlight below only swaps the brush — a thickness that changes
|
||||||
|
on hover would nudge the row's content while dragging over it. -->
|
||||||
|
<Setter Property="BorderThickness" Value="1.5" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Border.list-item:pointerover">
|
<Style Selector="Border.list-item:pointerover">
|
||||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||||
@@ -665,6 +704,11 @@
|
|||||||
<Style Selector="Border.list-item.active">
|
<Style Selector="Border.list-item.active">
|
||||||
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!-- Highlighted while a dragged task hovers over this row as a move target. -->
|
||||||
|
<Style Selector="Border.list-item.drop-target">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
<!-- Active item text / icon colors -->
|
<!-- Active item text / icon colors -->
|
||||||
<Style Selector="Border.list-item.active TextBlock.list-label">
|
<Style Selector="Border.list-item.active TextBlock.list-label">
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
@@ -997,6 +1041,11 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
|
<Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- manual → faint gray: a reminder Claude never touches -->
|
||||||
|
<Style Selector="Border.badge.manual">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ManualBadgeBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- SHARED MODAL STYLES (promoted from per-modal Window.Styles) -->
|
<!-- SHARED MODAL STYLES (promoted from per-modal Window.Styles) -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
@@ -1162,6 +1211,13 @@
|
|||||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!-- Disabled: drop the accent so a gated action (e.g. Approve before the diff is
|
||||||
|
opened) reads clearly as unavailable instead of looking clickable. -->
|
||||||
|
<Style Selector="Button.accent:disabled /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
|
||||||
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- DAY TOGGLE -->
|
<!-- DAY TOGGLE -->
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
public static class AccentPresetService
|
||||||
|
{
|
||||||
|
public static void Apply(AccentPreset preset)
|
||||||
|
{
|
||||||
|
if (Application.Current is not { } app) return;
|
||||||
|
|
||||||
|
SetBrushColor(app, "AccentBrush", preset.Accent);
|
||||||
|
SetBrushColor(app, "AccentDimBrush", preset.Dim);
|
||||||
|
SetBrushColor(app, "AccentSoftBrush", preset.Soft);
|
||||||
|
SetBrushColor(app, "AccentGlowBrush", preset.Glow);
|
||||||
|
SetBrushColor(app, "MossBrush", preset.Accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBrushColor(Application app, string key, string hex)
|
||||||
|
{
|
||||||
|
if (app.TryGetResource(key, null, out var value) && value is SolidColorBrush brush)
|
||||||
|
brush.Color = Color.Parse(hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,16 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
|
using ClaudeDo.Ui.Views;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears focus from a TextBox when the user clicks outside of any text box, so input
|
/// Clears focus from a TextBox when the user clicks outside of any text box, so input
|
||||||
/// fields behave like the user expects. Registered once for every window in the app.
|
/// fields behave like the user expects. Registered once for every window in the app.
|
||||||
|
/// Escape does the same, but only in the main window: modal windows bind Escape to closing
|
||||||
|
/// themselves, and Mission Control's ConPTY tiles need Escape to reach the terminal, so this
|
||||||
|
/// intentionally does not use a generic TopLevel handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FocusClearing
|
public static class FocusClearing
|
||||||
{
|
{
|
||||||
@@ -16,6 +20,9 @@ public static class FocusClearing
|
|||||||
{
|
{
|
||||||
InputElement.PointerPressedEvent.AddClassHandler<TopLevel>(
|
InputElement.PointerPressedEvent.AddClassHandler<TopLevel>(
|
||||||
OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||||
|
|
||||||
|
InputElement.KeyDownEvent.AddClassHandler<MainWindow>(
|
||||||
|
OnKeyDown, RoutingStrategies.Tunnel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e)
|
private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e)
|
||||||
@@ -31,4 +38,19 @@ public static class FocusClearing
|
|||||||
|
|
||||||
focusManager.Focus(null);
|
focusManager.Focus(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnKeyDown(MainWindow window, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key != Key.Escape)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (window.FocusManager is not { } focusManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (focusManager.GetFocusedElement() is not TextBox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
focusManager.Focus(null);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
@@ -18,10 +16,14 @@ public interface IDialogService
|
|||||||
{
|
{
|
||||||
Task ShowAboutAsync(AboutModalViewModel vm);
|
Task ShowAboutAsync(AboutModalViewModel vm);
|
||||||
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
||||||
|
Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm);
|
||||||
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
||||||
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
||||||
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
||||||
Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm);
|
Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm);
|
||||||
|
|
||||||
|
/// <summary>Merge-helper task picker. Returns the ordered selected task ids, or null on cancel.</summary>
|
||||||
|
Task<IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm);
|
||||||
Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm);
|
Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm);
|
||||||
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
||||||
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
||||||
@@ -34,7 +36,4 @@ public interface IDialogService
|
|||||||
|
|
||||||
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
||||||
void ShowMissionControl(MissionControlViewModel vm);
|
void ShowMissionControl(MissionControlViewModel vm);
|
||||||
|
|
||||||
/// <summary>Show a detached monitor in its own window; <paramref name="onClosed"/> re-docks it when that window closes.</summary>
|
|
||||||
void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
||||||
event Action<string, string>? TaskQuestionResolvedEvent;
|
event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
|
|
||||||
event Action<string>? InteractiveSessionStartedEvent;
|
|
||||||
event Action<string>? InteractiveSessionEndedEvent;
|
|
||||||
event Action<string, IReadOnlyList<string>>? InteractiveQueueChangedEvent;
|
|
||||||
event Action<string, string>? InteractiveMessageSentEvent;
|
|
||||||
|
|
||||||
event Action? PrepStartedEvent;
|
event Action? PrepStartedEvent;
|
||||||
event Action<string>? PrepLineEvent;
|
event Action<string>? PrepLineEvent;
|
||||||
event Action<bool>? PrepFinishedEvent;
|
event Action<bool>? PrepFinishedEvent;
|
||||||
@@ -51,10 +46,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
||||||
/// <summary>Answer a question a running task raised via AskUser.</summary>
|
/// <summary>Answer a question a running task raised via AskUser.</summary>
|
||||||
Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer);
|
Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer);
|
||||||
Task SendInteractiveMessageAsync(string taskId, string text);
|
|
||||||
Task RemoveQueuedInteractiveMessageAsync(string taskId, string text);
|
|
||||||
Task StopInteractiveSessionAsync(string taskId);
|
|
||||||
Task InterruptInteractiveSessionAsync(string taskId);
|
|
||||||
/// <summary>The question a running task is currently blocked on, if any (for re-attach).</summary>
|
/// <summary>The question a running task is currently blocked on, if any (for re-attach).</summary>
|
||||||
Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId);
|
Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId);
|
||||||
Task ResetTaskAsync(string taskId);
|
Task ResetTaskAsync(string taskId);
|
||||||
@@ -64,6 +55,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
||||||
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
||||||
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
||||||
|
Task<List<SessionSkillDto>> GetSessionSkillsAsync();
|
||||||
|
Task<List<string>> InstallSessionSkillAsync(string url);
|
||||||
|
Task UpdateSessionSkillAsync(string sourceUrl);
|
||||||
|
Task RemoveSessionSkillAsync(string sourceUrl);
|
||||||
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
||||||
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
||||||
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
||||||
@@ -79,7 +74,27 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
||||||
Task AbortConflictMergeAsync(string taskId);
|
Task AbortConflictMergeAsync(string taskId);
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
/// <summary>Commits an interactively-worked task's worktree and moves it to WaitingForReview
|
||||||
|
/// (the only path that flips a hand-driven ConPTY session into the review/merge pipeline).</summary>
|
||||||
|
Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default);
|
||||||
|
/// <summary>Launch spec for an embedded ConPTY terminal to open an interactive session
|
||||||
|
/// in a task's worktree (same worktree prep as an autonomous run).</summary>
|
||||||
|
Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||||
|
/// <summary>Launch spec for an ad-hoc interactive session in an arbitrary directory --
|
||||||
|
/// no task, no worktree.</summary>
|
||||||
|
Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
|
||||||
|
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
||||||
|
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.</summary>
|
||||||
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
||||||
|
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
||||||
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
||||||
|
Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default);
|
||||||
|
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
||||||
|
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
||||||
|
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||||
|
/// <summary>Resumes a planning session and returns its embedded-ConPTY launch spec.</summary>
|
||||||
|
Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
||||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||||
@@ -124,4 +139,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
||||||
Task SetOnlineInboxAuthAsync(string refreshToken);
|
Task SetOnlineInboxAuthAsync(string refreshToken);
|
||||||
Task ClearOnlineInboxAuthAsync();
|
Task ClearOnlineInboxAuthAsync();
|
||||||
|
|
||||||
|
/// <summary>Raised whenever the worker's usage poller ticks (success or failure).</summary>
|
||||||
|
event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||||
|
Task<UsageSnapshotDto?> GetUsageSnapshotAsync();
|
||||||
|
Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to);
|
||||||
|
Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Iciclecreek.Terminal;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thin wrapper around <see cref="TerminalControl.LaunchProcess()"/> — the library owns the
|
||||||
|
/// Porta.Pty spawn, keyboard input, rendering, resize, and focus end to end (see
|
||||||
|
/// <c>spikes/ConPtyTerminal/MainWindow.axaml.cs</c>, which proved this renders correctly and
|
||||||
|
/// stays responsive). We only:
|
||||||
|
/// 1) apply <see cref="TerminalLaunchDescriptor.Env"/> onto the current process environment
|
||||||
|
/// before launching — Porta.Pty inherits the calling process's environment and there is no
|
||||||
|
/// per-launch env seam on <see cref="TerminalControl"/>/<see cref="TerminalControl.LaunchProcess()"/> —
|
||||||
|
/// 2) relay the control's own <see cref="TerminalControl.ProcessExited"/> event and
|
||||||
|
/// <see cref="TerminalControl.Kill()"/> method.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PtyTerminalSession : IDisposable
|
||||||
|
{
|
||||||
|
private TerminalControl? _control;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public bool IsRunning { get; private set; }
|
||||||
|
public int? ExitCode { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Raised when the child process exits, on the UI thread, with its exit code.</summary>
|
||||||
|
public event EventHandler<int>? ProcessExited;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies <paramref name="descriptor"/>'s env vars to the current process, then drives
|
||||||
|
/// <paramref name="control"/> to launch it via <see cref="TerminalControl.LaunchProcess()"/>.
|
||||||
|
/// </summary>
|
||||||
|
public async Task StartAsync(TerminalLaunchDescriptor descriptor, TerminalControl control, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (_control is not null) throw new InvalidOperationException("Session already started.");
|
||||||
|
_control = control;
|
||||||
|
control.ProcessExited += OnControlProcessExited;
|
||||||
|
|
||||||
|
foreach (var (key, value) in descriptor.Env)
|
||||||
|
Environment.SetEnvironmentVariable(key, value);
|
||||||
|
|
||||||
|
control.Process = descriptor.Exe;
|
||||||
|
control.Args = new List<string>(descriptor.Args);
|
||||||
|
control.StartingDirectory = descriptor.Cwd;
|
||||||
|
|
||||||
|
await control.LaunchProcess();
|
||||||
|
IsRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnControlProcessExited(object? sender, ProcessExitedEventArgs e)
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
ExitCode = e.ExitCode;
|
||||||
|
ProcessExited?.Invoke(this, e.ExitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Kill()
|
||||||
|
{
|
||||||
|
try { _control?.Kill(); }
|
||||||
|
catch (Exception) { /* already exited */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (_control is not null)
|
||||||
|
_control.ProcessExited -= OnControlProcessExited;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
/// <summary>Plain launch spec for an embedded ConPTY terminal session — no worker/SignalR coupling.</summary>
|
||||||
|
public sealed record TerminalLaunchDescriptor(
|
||||||
|
string Cwd,
|
||||||
|
string Exe,
|
||||||
|
IReadOnlyList<string> Args,
|
||||||
|
IReadOnlyDictionary<string, string> Env);
|
||||||
@@ -49,10 +49,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public event Action<string>? TaskUpdatedEvent;
|
public event Action<string>? TaskUpdatedEvent;
|
||||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
public event Action<string>? InteractiveSessionStartedEvent;
|
|
||||||
public event Action<string>? InteractiveSessionEndedEvent;
|
|
||||||
public event Action<string, IReadOnlyList<string>>? InteractiveQueueChangedEvent;
|
|
||||||
public event Action<string, string>? InteractiveMessageSentEvent;
|
|
||||||
public event Action? ConnectionRestoredEvent;
|
public event Action? ConnectionRestoredEvent;
|
||||||
public event Action<string>? WorktreeUpdatedEvent;
|
public event Action<string>? WorktreeUpdatedEvent;
|
||||||
public event Action<string>? ListUpdatedEvent;
|
public event Action<string>? ListUpdatedEvent;
|
||||||
@@ -65,6 +61,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public event Action<string>? RefineStartedEvent;
|
public event Action<string>? RefineStartedEvent;
|
||||||
public event Action<string, bool, string?>? RefineFinishedEvent;
|
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||||
|
|
||||||
|
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||||
|
|
||||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||||
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
||||||
@@ -152,26 +150,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
||||||
});
|
});
|
||||||
|
|
||||||
_hub.On<string>("InteractiveSessionStarted", taskId =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => InteractiveSessionStartedEvent?.Invoke(taskId));
|
|
||||||
});
|
|
||||||
|
|
||||||
_hub.On<string>("InteractiveSessionEnded", taskId =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => InteractiveSessionEndedEvent?.Invoke(taskId));
|
|
||||||
});
|
|
||||||
|
|
||||||
_hub.On<string, IReadOnlyList<string>>("InteractiveQueueChanged", (taskId, pending) =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => InteractiveQueueChangedEvent?.Invoke(taskId, pending));
|
|
||||||
});
|
|
||||||
|
|
||||||
_hub.On<string, string>("InteractiveMessageSent", (taskId, text) =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => InteractiveMessageSentEvent?.Invoke(taskId, text));
|
|
||||||
});
|
|
||||||
|
|
||||||
_hub.On<string>("WorktreeUpdated", taskId =>
|
_hub.On<string>("WorktreeUpdated", taskId =>
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
|
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
|
||||||
@@ -226,6 +204,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
||||||
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
||||||
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
||||||
|
|
||||||
|
_hub.On<UsageSnapshotDto>("UsageUpdated", snapshot =>
|
||||||
|
Dispatcher.UIThread.Post(() => UsageUpdatedEvent?.Invoke(snapshot)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync()
|
public Task StartAsync()
|
||||||
@@ -303,30 +284,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
catch { /* offline or already resolved — the UI clears optimistically */ }
|
catch { /* offline or already resolved — the UI clears optimistically */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendInteractiveMessageAsync(string taskId, string text)
|
|
||||||
{
|
|
||||||
try { await _hub.InvokeAsync("SendInteractiveMessage", taskId, text); }
|
|
||||||
catch { /* offline or session already ended */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveQueuedInteractiveMessageAsync(string taskId, string text)
|
|
||||||
{
|
|
||||||
try { await _hub.InvokeAsync("RemoveQueuedInteractiveMessage", taskId, text); }
|
|
||||||
catch { /* offline or session already ended */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StopInteractiveSessionAsync(string taskId)
|
|
||||||
{
|
|
||||||
try { await _hub.InvokeAsync("StopInteractiveSession", taskId); }
|
|
||||||
catch { /* offline */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InterruptInteractiveSessionAsync(string taskId)
|
|
||||||
{
|
|
||||||
try { await _hub.InvokeAsync("InterruptInteractiveSession", taskId); }
|
|
||||||
catch { /* offline */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
|
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
|
||||||
=> TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);
|
=> TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);
|
||||||
|
|
||||||
@@ -377,8 +334,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("RefreshAgents");
|
await _hub.InvokeAsync("RefreshAgents");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||||
=> TryInvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
=> await _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||||
|
|
||||||
private async Task SeedActiveTasksAsync()
|
private async Task SeedActiveTasksAsync()
|
||||||
{
|
{
|
||||||
@@ -420,8 +377,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
||||||
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
||||||
|
|
||||||
public Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
||||||
=> TryInvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
=> await _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
||||||
|
|
||||||
public async Task DeletePrimeScheduleAsync(Guid id)
|
public async Task DeletePrimeScheduleAsync(Guid id)
|
||||||
{
|
{
|
||||||
@@ -448,8 +405,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
||||||
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
||||||
|
|
||||||
public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
||||||
=> TryInvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
=> await _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
||||||
|
|
||||||
public async Task UpdateDailyNoteAsync(string id, string text)
|
public async Task UpdateDailyNoteAsync(string id, string text)
|
||||||
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
||||||
@@ -481,15 +438,27 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SessionSkillDto>> GetSessionSkillsAsync()
|
||||||
|
=> await TryInvokeAsync<List<SessionSkillDto>>("GetSessionSkills") ?? [];
|
||||||
|
|
||||||
|
public Task<List<string>> InstallSessionSkillAsync(string url)
|
||||||
|
=> _hub.InvokeAsync<List<string>>("InstallSessionSkill", url);
|
||||||
|
|
||||||
|
public Task UpdateSessionSkillAsync(string sourceUrl)
|
||||||
|
=> _hub.InvokeAsync("UpdateSessionSkill", sourceUrl);
|
||||||
|
|
||||||
|
public Task RemoveSessionSkillAsync(string sourceUrl)
|
||||||
|
=> _hub.InvokeAsync("RemoveSessionSkill", sourceUrl);
|
||||||
|
|
||||||
public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
||||||
{
|
{
|
||||||
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
|
public async Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
|
||||||
{
|
{
|
||||||
LastApproveTarget = targetBranch;
|
LastApproveTarget = targetBranch;
|
||||||
return TryInvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
return await _hub.InvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
|
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
|
||||||
@@ -510,11 +479,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("CancelReview", taskId);
|
await _hub.InvokeAsync("CancelReview", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||||
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
=> await _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||||
|
|
||||||
public Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||||
=> TryInvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
=> await _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||||
|
|
||||||
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
||||||
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
||||||
@@ -537,8 +506,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
||||||
=> TryInvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
=> await _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
||||||
|
|
||||||
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
||||||
@@ -546,8 +515,27 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
||||||
|
|
||||||
public async Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default)
|
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync("OpenInteractiveTerminalAsync", taskId, ct);
|
=> await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct);
|
||||||
|
|
||||||
|
public async Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct);
|
||||||
|
|
||||||
|
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct);
|
||||||
|
|
||||||
|
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
||||||
|
|
||||||
|
public async Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct);
|
||||||
|
|
||||||
|
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
||||||
|
|
||||||
|
public async Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct);
|
||||||
|
|
||||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
||||||
@@ -561,8 +549,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||||
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
||||||
|
|
||||||
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||||
=> TryInvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
=> await _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||||
|
|
||||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||||
{
|
{
|
||||||
@@ -591,6 +579,15 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task ClearOnlineInboxAuthAsync()
|
public async Task ClearOnlineInboxAuthAsync()
|
||||||
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
||||||
|
|
||||||
|
public Task<UsageSnapshotDto?> GetUsageSnapshotAsync()
|
||||||
|
=> TryInvokeAsync<UsageSnapshotDto>("GetUsageSnapshot");
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
|
||||||
|
=> await TryInvokeAsync<List<ModelUsageRowDto>>("GetModelUsage", from, to) ?? [];
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
|
||||||
|
=> await TryInvokeAsync<List<TaskUsageRowDto>>("GetTaskUsage", from, to) ?? [];
|
||||||
|
|
||||||
// IWorkerClient explicit implementations (drop typed return values)
|
// IWorkerClient explicit implementations (drop typed return values)
|
||||||
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
||||||
=> await StartPlanningSessionAsync(taskId, ct);
|
=> await StartPlanningSessionAsync(taskId, ct);
|
||||||
@@ -624,7 +621,21 @@ public sealed record AppSettingsDto(
|
|||||||
int WorktreeAutoCleanupDays,
|
int WorktreeAutoCleanupDays,
|
||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks);
|
int DailyPrepMaxTasks,
|
||||||
|
List<string>? SessionSkills = null,
|
||||||
|
List<ModelPresetDto>? ModelPresets = null,
|
||||||
|
int UsageGateFiveHourPct = 80,
|
||||||
|
int UsageGateSevenDayPct = 90);
|
||||||
|
|
||||||
|
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
||||||
|
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
|
|
||||||
|
public sealed record SessionSkillDto(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string SourceUrl,
|
||||||
|
string PinnedRef,
|
||||||
|
DateTimeOffset AddedAt);
|
||||||
|
|
||||||
public sealed record WorktreeCleanupDto(int Removed);
|
public sealed record WorktreeCleanupDto(int Removed);
|
||||||
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
@@ -634,10 +645,10 @@ public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalB
|
|||||||
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
|
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
|
||||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||||
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false);
|
||||||
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public sealed record SeedResultDto(int Copied, int Skipped);
|
public sealed record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public sealed record WorktreeOverviewDto(
|
public sealed record WorktreeOverviewDto(
|
||||||
@@ -654,6 +665,12 @@ public sealed record WorktreeOverviewDto(
|
|||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
bool PathExistsOnDisk);
|
bool PathExistsOnDisk);
|
||||||
|
|
||||||
|
public sealed record LaunchSpec(
|
||||||
|
string Cwd,
|
||||||
|
string Exe,
|
||||||
|
IReadOnlyList<string> Args,
|
||||||
|
IReadOnlyDictionary<string, string> Env);
|
||||||
|
|
||||||
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
|
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
|
||||||
public sealed record PendingQuestionDto(string TaskId, string QuestionId, string Question);
|
public sealed record PendingQuestionDto(string TaskId, string QuestionId, string Question);
|
||||||
|
|
||||||
@@ -675,3 +692,46 @@ public sealed record OnlineInboxConfigInputDto(
|
|||||||
string ClientId,
|
string ClientId,
|
||||||
string Scopes,
|
string Scopes,
|
||||||
string RedirectUri);
|
string RedirectUri);
|
||||||
|
|
||||||
|
public sealed record UsageLimitDto(
|
||||||
|
string Kind,
|
||||||
|
string Group,
|
||||||
|
double Percent,
|
||||||
|
string Severity,
|
||||||
|
DateTimeOffset? ResetsAt,
|
||||||
|
string? ScopeModelDisplayName,
|
||||||
|
bool IsActive);
|
||||||
|
|
||||||
|
public sealed record UsageSnapshotDto(
|
||||||
|
double? FiveHourPercent,
|
||||||
|
DateTimeOffset? FiveHourResetsAt,
|
||||||
|
double? SevenDayPercent,
|
||||||
|
DateTimeOffset? SevenDayResetsAt,
|
||||||
|
IReadOnlyList<UsageLimitDto> Limits,
|
||||||
|
int FiveHourThresholdPct,
|
||||||
|
int SevenDayThresholdPct,
|
||||||
|
bool IsGateBlocked,
|
||||||
|
string? GateReason,
|
||||||
|
DateTime? FetchedAtUtc,
|
||||||
|
bool IsStale,
|
||||||
|
string? LastError);
|
||||||
|
|
||||||
|
public sealed record ModelUsageRowDto(
|
||||||
|
DateOnly Date,
|
||||||
|
string Model,
|
||||||
|
string Scope,
|
||||||
|
long InputTokens,
|
||||||
|
long OutputTokens,
|
||||||
|
long CacheReadTokens,
|
||||||
|
long CacheCreationTokens,
|
||||||
|
int Messages);
|
||||||
|
|
||||||
|
public sealed record TaskUsageRowDto(
|
||||||
|
string TaskId,
|
||||||
|
string TaskTitle,
|
||||||
|
string ListId,
|
||||||
|
string ListName,
|
||||||
|
string? Model,
|
||||||
|
int Runs,
|
||||||
|
long TokensIn,
|
||||||
|
long TokensOut);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Text.Json;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
@@ -44,7 +47,11 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||||
|
|
||||||
private string _globalModel = ModelRegistry.DefaultAlias;
|
private string _globalModel = ModelRegistry.DefaultAlias;
|
||||||
private int _globalMaxTurns = 100;
|
// The global max-turns default is per-model (Settings -> General), so it moves with whichever
|
||||||
|
// model actually ends up in effect here.
|
||||||
|
private IReadOnlyList<ModelPreset> _presets = ModelPresets.Defaults;
|
||||||
|
private string EffectiveModel => Model ?? _listModel ?? _globalModel;
|
||||||
|
private int GlobalMaxTurns => ModelPresets.For(_presets, EffectiveModel).MaxTurns;
|
||||||
private string? _listModel; // Task scope only
|
private string? _listModel; // Task scope only
|
||||||
private int? _listMaxTurns; // Task scope only
|
private int? _listMaxTurns; // Task scope only
|
||||||
private string? _listAgentName; // Task scope only
|
private string? _listAgentName; // Task scope only
|
||||||
@@ -53,10 +60,11 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
private CancellationTokenSource? _saveCts;
|
private CancellationTokenSource? _saveCts;
|
||||||
|
|
||||||
public int EffectiveMaxTurns =>
|
public int EffectiveMaxTurns =>
|
||||||
MaxTurns is decimal t ? (int)t : (_listMaxTurns ?? _globalMaxTurns);
|
MaxTurns is decimal t ? (int)t : (_listMaxTurns ?? GlobalMaxTurns);
|
||||||
|
|
||||||
public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
|
public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||||
public ObservableCollection<AgentInfo> Agents { get; } = new();
|
public ObservableCollection<AgentInfo> Agents { get; } = new();
|
||||||
|
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
|
||||||
|
|
||||||
public AgentConfigEditorViewModel(IWorkerClient worker, AgentConfigScope scope)
|
public AgentConfigEditorViewModel(IWorkerClient worker, AgentConfigScope scope)
|
||||||
{
|
{
|
||||||
@@ -67,11 +75,40 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
// short-lived modal recreated with the current language on each open.
|
// short-lived modal recreated with the current language on each open.
|
||||||
if (scope == AgentConfigScope.Task)
|
if (scope == AgentConfigScope.Task)
|
||||||
Loc.LanguageChanged += _langChangedHandler;
|
Loc.LanguageChanged += _langChangedHandler;
|
||||||
|
|
||||||
|
SessionSkills.CollectionChanged += OnSessionSkillsCollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => Loc.LanguageChanged -= _langChangedHandler;
|
private void OnSessionSkillsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.NewItems is not null)
|
||||||
|
foreach (SelectableSkillViewModel item in e.NewItems)
|
||||||
|
item.PropertyChanged += OnSkillItemPropertyChanged;
|
||||||
|
if (e.OldItems is not null)
|
||||||
|
foreach (SelectableSkillViewModel item in e.OldItems)
|
||||||
|
item.PropertyChanged -= OnSkillItemPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnModelChanged(string? value) { RecomputeModelBadge(); QueueSave(); }
|
private void OnSkillItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(SelectableSkillViewModel.IsSelected)) QueueSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Loc.LanguageChanged -= _langChangedHandler;
|
||||||
|
SessionSkills.CollectionChanged -= OnSessionSkillsCollectionChanged;
|
||||||
|
foreach (var s in SessionSkills) s.PropertyChanged -= OnSkillItemPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnModelChanged(string? value)
|
||||||
|
{
|
||||||
|
RecomputeModelBadge();
|
||||||
|
// A different model means a different global turn default.
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
||||||
|
QueueSave();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnMaxTurnsChanged(decimal? value)
|
partial void OnMaxTurnsChanged(decimal? value)
|
||||||
{
|
{
|
||||||
@@ -104,8 +141,8 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
{
|
{
|
||||||
var own = MaxTurns?.ToString();
|
var own = MaxTurns?.ToString();
|
||||||
var (value, source) = _scope == AgentConfigScope.Task
|
var (value, source) = _scope == AgentConfigScope.Task
|
||||||
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), _globalMaxTurns.ToString())
|
? InheritanceResolver.Resolve(own, _listMaxTurns?.ToString(), GlobalMaxTurns.ToString())
|
||||||
: InheritanceResolver.ResolveList(own, _globalMaxTurns.ToString());
|
: InheritanceResolver.ResolveList(own, GlobalMaxTurns.ToString());
|
||||||
TurnsInheritedHint = value;
|
TurnsInheritedHint = value;
|
||||||
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
||||||
}
|
}
|
||||||
@@ -147,18 +184,28 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async System.Threading.Tasks.Task SaveAsync()
|
// verifyCommand is a List-only field owned by ListSettingsModalViewModel (not this editor,
|
||||||
|
// which is also reused for Task scope); the caller passes it through so the single
|
||||||
|
// UpdateListConfig call carries the full desired row instead of clobbering it.
|
||||||
|
public async System.Threading.Tasks.Task SaveAsync(string? verifyCommand = null)
|
||||||
{
|
{
|
||||||
if (TargetId is null) return;
|
if (TargetId is null) return;
|
||||||
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
||||||
var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
|
var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
|
||||||
var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
|
var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
|
||||||
var turns = MaxTurns is decimal d ? (int?)d : null;
|
var turns = MaxTurns is decimal d ? (int?)d : null;
|
||||||
|
var skills = SelectedSessionSkillNames();
|
||||||
|
|
||||||
if (_scope == AgentConfigScope.Task)
|
if (_scope == AgentConfigScope.Task)
|
||||||
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns));
|
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills));
|
||||||
else
|
else
|
||||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns));
|
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string>? SelectedSessionSkillNames()
|
||||||
|
{
|
||||||
|
var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
||||||
|
return names.Count == 0 ? null : names;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async System.Threading.Tasks.Task LoadForListAsync(string listId, CancellationToken ct = default)
|
public async System.Threading.Tasks.Task LoadForListAsync(string listId, CancellationToken ct = default)
|
||||||
@@ -172,6 +219,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
|
|
||||||
var cfg = await _worker.GetListConfigAsync(listId);
|
var cfg = await _worker.GetListConfigAsync(listId);
|
||||||
ApplyConfig(cfg?.Model, cfg?.MaxTurns, cfg?.SystemPrompt, cfg?.AgentPath);
|
ApplyConfig(cfg?.Model, cfg?.MaxTurns, cfg?.SystemPrompt, cfg?.AgentPath);
|
||||||
|
await ReloadSessionSkillsAsync(cfg?.SessionSkills);
|
||||||
|
|
||||||
_listModel = null; _listMaxTurns = null; _listAgentName = null;
|
_listModel = null; _listMaxTurns = null; _listAgentName = null;
|
||||||
EffectiveSystemPromptHint = "";
|
EffectiveSystemPromptHint = "";
|
||||||
@@ -189,6 +237,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
TargetId = entity.Id;
|
TargetId = entity.Id;
|
||||||
await ReloadAgentsAsync("(inherited)");
|
await ReloadAgentsAsync("(inherited)");
|
||||||
ApplyConfig(entity.Model, entity.MaxTurns, entity.SystemPrompt, entity.AgentPath);
|
ApplyConfig(entity.Model, entity.MaxTurns, entity.SystemPrompt, entity.AgentPath);
|
||||||
|
await ReloadSessionSkillsAsync(ParseSessionSkills(entity.SessionSkills));
|
||||||
|
|
||||||
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
||||||
await LoadGlobalDefaultsAsync();
|
await LoadGlobalDefaultsAsync();
|
||||||
@@ -214,12 +263,30 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
MaxTurns = null;
|
MaxTurns = null;
|
||||||
SystemPrompt = "";
|
SystemPrompt = "";
|
||||||
SelectedAgent = null;
|
SelectedAgent = null;
|
||||||
|
foreach (var s in SessionSkills) s.IsSelected = false;
|
||||||
}
|
}
|
||||||
finally { _suppressSave = false; }
|
finally { _suppressSave = false; }
|
||||||
EffectiveSystemPromptHint = "";
|
EffectiveSystemPromptHint = "";
|
||||||
TargetId = null;
|
TargetId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string>? ParseSessionSkills(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||||
|
try { return JsonSerializer.Deserialize<List<string>>(json); }
|
||||||
|
catch (JsonException) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task ReloadSessionSkillsAsync(IReadOnlyCollection<string>? selected)
|
||||||
|
{
|
||||||
|
var installed = await _worker.GetSessionSkillsAsync();
|
||||||
|
var selectedSet = selected is null ? new HashSet<string>() : new HashSet<string>(selected);
|
||||||
|
foreach (var s in SessionSkills) s.PropertyChanged -= OnSkillItemPropertyChanged;
|
||||||
|
SessionSkills.Clear();
|
||||||
|
foreach (var s in installed)
|
||||||
|
SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name)));
|
||||||
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task ReloadAgentsAsync(string placeholderName)
|
private async System.Threading.Tasks.Task ReloadAgentsAsync(string placeholderName)
|
||||||
{
|
{
|
||||||
Agents.Clear();
|
Agents.Clear();
|
||||||
@@ -231,7 +298,9 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
{
|
{
|
||||||
var app = await _worker.GetAppSettingsAsync();
|
var app = await _worker.GetAppSettingsAsync();
|
||||||
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
||||||
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
_presets = app?.ModelPresets is { Count: > 0 } rows
|
||||||
|
? rows.Select(r => new ModelPreset(r.Model, r.Effort, r.MaxTurns)).ToList()
|
||||||
|
: ModelPresets.Defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath)
|
private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath)
|
||||||
@@ -255,5 +324,6 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
MaxTurns = null;
|
MaxTurns = null;
|
||||||
SystemPrompt = "";
|
SystemPrompt = "";
|
||||||
SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
|
SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
|
||||||
|
foreach (var s in SessionSkills) s.IsSelected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Agent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One installed session skill shown as a checkbox row. Reused by the global (General tab),
|
||||||
|
/// list, and task selectors — selection is additive-union across all three levels, so this
|
||||||
|
/// is deliberately just a name + checked flag with no inheritance/override state.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class SelectableSkillViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
|
||||||
|
public SelectableSkillViewModel(string name, string description = "", bool isSelected = false)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
_isSelected = isSelected;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,9 +103,19 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ContinueHint => HasBinaryFiles
|
public string ContinueHint
|
||||||
? "Binary conflicts must be resolved externally — abort and resolve in your editor."
|
{
|
||||||
: "";
|
get
|
||||||
|
{
|
||||||
|
if (HasBinaryFiles)
|
||||||
|
return "Binary conflicts must be resolved externally — abort and resolve in your editor.";
|
||||||
|
var openFiles = Files.Count(f => !f.AllResolved);
|
||||||
|
if (openFiles == 0) return "";
|
||||||
|
var openConflicts = _flat.Count(x => !x.Block.IsResolved);
|
||||||
|
return $"{openConflicts} {(openConflicts == 1 ? "conflict" : "conflicts")} in " +
|
||||||
|
$"{openFiles} {(openFiles == 1 ? "file" : "files")} still unresolved.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool InRange => CurrentIndex >= 0 && CurrentIndex < _flat.Count;
|
private bool InRange => CurrentIndex >= 0 && CurrentIndex < _flat.Count;
|
||||||
|
|
||||||
@@ -188,6 +198,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(HasBinaryFiles));
|
OnPropertyChanged(nameof(HasBinaryFiles));
|
||||||
OnPropertyChanged(nameof(HasMultipleFiles));
|
OnPropertyChanged(nameof(HasMultipleFiles));
|
||||||
OnPropertyChanged(nameof(FilesSummary));
|
OnPropertyChanged(nameof(FilesSummary));
|
||||||
|
OnPropertyChanged(nameof(ContinueHint));
|
||||||
RecomputeCanContinue();
|
RecomputeCanContinue();
|
||||||
if (_flat.Count > 0)
|
if (_flat.Count > 0)
|
||||||
MoveTo(0); // also sets ActiveFile via MoveTo
|
MoveTo(0); // also sets ActiveFile via MoveTo
|
||||||
@@ -211,6 +222,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(PositionText));
|
OnPropertyChanged(nameof(PositionText));
|
||||||
OnPropertyChanged(nameof(ActiveResultText));
|
OnPropertyChanged(nameof(ActiveResultText));
|
||||||
OnPropertyChanged(nameof(FilesSummary));
|
OnPropertyChanged(nameof(FilesSummary));
|
||||||
|
OnPropertyChanged(nameof(ContinueHint));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using Iciclecreek.Terminal;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hosts a <see cref="PtyTerminalSession"/> for an embedded ConPTY terminal. The view attaches
|
||||||
|
/// its <see cref="TerminalControl"/> once loaded via <see cref="AttachControl"/>; <see cref="Start"/>
|
||||||
|
/// can be called before or after attach — whichever happens second triggers the launch.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly PtyTerminalSession _session = new();
|
||||||
|
private TerminalControl? _control;
|
||||||
|
private TerminalLaunchDescriptor? _pendingDescriptor;
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isRunning;
|
||||||
|
[ObservableProperty] private bool _hasExited;
|
||||||
|
[ObservableProperty] private int? _exitCode;
|
||||||
|
[ObservableProperty] private string? _startError;
|
||||||
|
|
||||||
|
/// <summary>True from construction until the child process is actually launched (or the launch
|
||||||
|
/// failed) — covers both the caller's launch-spec roundtrip and the ConPTY spawn, so the host
|
||||||
|
/// can show a spinner instead of an empty black pane.</summary>
|
||||||
|
public bool IsStarting => !IsRunning && !HasExited && StartError is null;
|
||||||
|
|
||||||
|
partial void OnIsRunningChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
partial void OnHasExitedChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
partial void OnStartErrorChanged(string? value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
|
||||||
|
public InteractiveTerminalViewModel()
|
||||||
|
{
|
||||||
|
_session.ProcessExited += OnSessionProcessExited;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called by the view once its <see cref="TerminalControl"/> has loaded (template applied).</summary>
|
||||||
|
public void AttachControl(TerminalControl control)
|
||||||
|
{
|
||||||
|
_control = control;
|
||||||
|
if (_pendingDescriptor is { } descriptor)
|
||||||
|
{
|
||||||
|
_pendingDescriptor = null;
|
||||||
|
_ = StartCoreAsync(descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(TerminalLaunchDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (_control is null)
|
||||||
|
{
|
||||||
|
_pendingDescriptor = descriptor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ = StartCoreAsync(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartCoreAsync(TerminalLaunchDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (_control is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StartError = null;
|
||||||
|
await _session.StartAsync(descriptor, _control);
|
||||||
|
IsRunning = true;
|
||||||
|
HasExited = false;
|
||||||
|
ExitCode = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
HasExited = true;
|
||||||
|
StartError = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSessionProcessExited(object? sender, int exitCode)
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
HasExited = true;
|
||||||
|
ExitCode = exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reports a failure that happened before <see cref="Start"/> could be called (e.g. the
|
||||||
|
/// launch-spec roundtrip threw), so it surfaces through the same banner as a spawn failure.</summary>
|
||||||
|
public void ReportStartFailure(string message)
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
HasExited = true;
|
||||||
|
StartError = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Kill() => _session.Kill();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_session.ProcessExited -= OnSessionProcessExited;
|
||||||
|
_session.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,8 +191,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanReplyToRoadblock))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))]
|
||||||
private string? _latestRunSessionId;
|
private string? _latestRunSessionId;
|
||||||
|
|
||||||
|
// A resumable session to reply to a reported roadblock with (via ContinueAsync's
|
||||||
|
// transport). Distinct from ContinueCommand: that one is Failed/Cancelled-only and
|
||||||
|
// reruns with a fixed prompt, this one answers a roadblock reported on any terminal
|
||||||
|
// state (see ShowRoadblockCard) with the user's own text.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))]
|
||||||
|
private string _roadblockReplyDraft = string.Empty;
|
||||||
|
|
||||||
|
public bool CanReplyToRoadblock => !string.IsNullOrEmpty(LatestRunSessionId);
|
||||||
|
|
||||||
|
// Surfaces a UI-originated failure (e.g. override slot busy) to the shell's footer
|
||||||
|
// error strip — mirrors TasksIslandViewModel.ErrorReported.
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
[ObservableProperty] private string? _worktreePath;
|
||||||
@@ -258,6 +274,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private bool _suppressDescSave;
|
private bool _suppressDescSave;
|
||||||
private CancellationTokenSource? _descSaveCts;
|
private CancellationTokenSource? _descSaveCts;
|
||||||
|
|
||||||
|
private bool _suppressTitleSave;
|
||||||
|
private CancellationTokenSource? _titleSaveCts;
|
||||||
|
|
||||||
// Set by shell so CloseDetailCommand can clear SelectedTask
|
// Set by shell so CloseDetailCommand can clear SelectedTask
|
||||||
public Action? CloseDetail { get; set; }
|
public Action? CloseDetail { get; set; }
|
||||||
|
|
||||||
@@ -296,9 +315,19 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
||||||
Merge = new MergeSectionViewModel(worker, services);
|
Merge = new MergeSectionViewModel(worker, services);
|
||||||
|
Merge.DiffViewed = () => ReviewDiffViewed = true;
|
||||||
|
Merge.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(MergeSectionViewModel.HasReviewableDiff))
|
||||||
|
{
|
||||||
|
ApproveReviewCommand.NotifyCanExecuteChanged();
|
||||||
|
OnPropertyChanged(nameof(ShowReviewDiffHint));
|
||||||
|
}
|
||||||
|
};
|
||||||
Prep = new PrepPanelViewModel(worker);
|
Prep = new PrepPanelViewModel(worker);
|
||||||
|
|
||||||
Notes = new NotesEditorViewModel(_notesApi);
|
Notes = new NotesEditorViewModel(_notesApi);
|
||||||
|
Notes.ErrorReported += msg => { if (ShowErrorAsync is not null) _ = ShowErrorAsync(msg); };
|
||||||
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
||||||
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||||
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
|
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
|
||||||
@@ -320,6 +349,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
DequeueCommand.NotifyCanExecuteChanged();
|
DequeueCommand.NotifyCanExecuteChanged();
|
||||||
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
||||||
ContinueCommand.NotifyCanExecuteChanged();
|
ContinueCommand.NotifyCanExecuteChanged();
|
||||||
|
SendRoadblockReplyCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
||||||
@@ -350,6 +380,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
|
// The bound row's own status can change from outside (worker run, MCP tool, another
|
||||||
|
// session) — refresh it directly instead of relying on TasksIslandViewModel mutating
|
||||||
|
// the same instance, so the detail pane stays correct even if that row was replaced.
|
||||||
|
if (Task?.Id == taskId) _ = RefreshBoundTaskAsync(taskId);
|
||||||
};
|
};
|
||||||
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
||||||
|
|
||||||
@@ -399,6 +433,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
DequeueCommand.NotifyCanExecuteChanged();
|
DequeueCommand.NotifyCanExecuteChanged();
|
||||||
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
||||||
ContinueCommand.NotifyCanExecuteChanged();
|
ContinueCommand.NotifyCanExecuteChanged();
|
||||||
|
// A state change means a new run/review cycle: the diff must be
|
||||||
|
// re-inspected before merge can be approved again.
|
||||||
|
ReviewDiffViewed = false;
|
||||||
|
ApproveReviewCommand.NotifyCanExecuteChanged();
|
||||||
|
OnPropertyChanged(nameof(ShowReviewDiffHint));
|
||||||
|
OnPropertyChanged(nameof(CanSubmitForReview));
|
||||||
|
SubmitForReviewCommand.NotifyCanExecuteChanged();
|
||||||
AgentSettings.IsRunning = IsRunning;
|
AgentSettings.IsRunning = IsRunning;
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
@@ -414,6 +455,39 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnEditableTitleChanged(string value)
|
||||||
|
{
|
||||||
|
if (_suppressTitleSave || Task is null) return;
|
||||||
|
_titleSaveCts?.Cancel();
|
||||||
|
_titleSaveCts = new CancellationTokenSource();
|
||||||
|
// Capture the row so a task switch mid-debounce cannot retitle the new task.
|
||||||
|
_ = SaveTitleAsync(Task, value, _titleSaveCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task SaveTitleAsync(
|
||||||
|
TaskRowViewModel row, string value, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await System.Threading.Tasks.Task.Delay(400, ct);
|
||||||
|
var title = value?.Trim();
|
||||||
|
// An empty title would make the row unidentifiable — keep the stored one.
|
||||||
|
if (string.IsNullOrEmpty(title)) return;
|
||||||
|
await using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var repo = new TaskRepository(ctx);
|
||||||
|
var entity = await repo.GetByIdAsync(row.Id);
|
||||||
|
if (entity is null || entity.Title == title) return;
|
||||||
|
entity.Title = title;
|
||||||
|
await repo.UpdateAsync(entity);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
row.Title = title;
|
||||||
|
if (ReferenceEquals(Task, row))
|
||||||
|
Merge.SyncTaskContext(row.Id, title, row.IsPlanningParent);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnEditableDescriptionChanged(string value)
|
partial void OnEditableDescriptionChanged(string value)
|
||||||
{
|
{
|
||||||
if (_suppressDescSave || Task is null) return;
|
if (_suppressDescSave || Task is null) return;
|
||||||
@@ -467,6 +541,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Task = row;
|
Task = row;
|
||||||
OnPropertyChanged(nameof(TaskIdBadge));
|
OnPropertyChanged(nameof(TaskIdBadge));
|
||||||
Monitor.Reset();
|
Monitor.Reset();
|
||||||
|
RoadblockReplyDraft = string.Empty;
|
||||||
Subtasks.Clear();
|
Subtasks.Clear();
|
||||||
ChildOutcomes.Clear();
|
ChildOutcomes.Clear();
|
||||||
Attachments.Clear();
|
Attachments.Clear();
|
||||||
@@ -509,15 +584,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
if (entity == null) return;
|
if (entity == null) return;
|
||||||
|
|
||||||
EditableTitle = entity.Title;
|
_suppressTitleSave = true;
|
||||||
|
try { EditableTitle = entity.Title; }
|
||||||
|
finally { _suppressTitleSave = false; }
|
||||||
_suppressDescSave = true;
|
_suppressDescSave = true;
|
||||||
try { EditableDescription = entity.Description ?? ""; }
|
try { EditableDescription = entity.Description ?? ""; }
|
||||||
finally { _suppressDescSave = false; }
|
finally { _suppressDescSave = false; }
|
||||||
Model = entity.Model;
|
Model = entity.Model;
|
||||||
_listWorkingDir = entity.List?.WorkingDir;
|
_listWorkingDir = entity.List?.WorkingDir;
|
||||||
WorktreePath = entity.Worktree?.Path;
|
WorktreePath = entity.Worktree?.Path;
|
||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
// A worktree-less list-handler host task (Mission Control's "Let Claude handle it")
|
||||||
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
// has no WorktreeEntity, so its review range falls back to HandlerBaseCommit/
|
||||||
|
// HandlerHeadCommit -- see TaskEntity for why.
|
||||||
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
|
||||||
|
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
@@ -536,6 +616,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
LatestRunSessionId = latestRun?.SessionId;
|
LatestRunSessionId = latestRun?.SessionId;
|
||||||
|
// Restore turn/token counts from the last run so a reloaded terminal task
|
||||||
|
// shows its real turns instead of "0/max".
|
||||||
|
Turns = latestRun?.TurnCount ?? 0;
|
||||||
|
Tokens = (latestRun?.TokensIn ?? 0) + (latestRun?.TokensOut ?? 0);
|
||||||
Monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
Monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
||||||
|
|
||||||
Monitor.SetTaskId(row.Id);
|
Monitor.SetTaskId(row.Id);
|
||||||
@@ -705,6 +789,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refreshes the bound row itself (status, planning phase, worktree/branch mirrors, etc.) from
|
||||||
|
// the DB. Kept independent of TasksIslandViewModel's own handler: the row instance backing
|
||||||
|
// Task may have been replaced by a full list reload, so this must not assume it stayed live.
|
||||||
|
private async System.Threading.Tasks.Task RefreshBoundTaskAsync(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.Include(t => t.List)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
if (entity is null || Task?.Id != taskId) return;
|
||||||
|
Task.UpdateFromEntity(entity);
|
||||||
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
|
}
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -719,8 +823,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
_listWorkingDir = entity.List?.WorkingDir;
|
_listWorkingDir = entity.List?.WorkingDir;
|
||||||
WorktreePath = entity.Worktree?.Path;
|
WorktreePath = entity.Worktree?.Path;
|
||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
// A worktree-less list-handler host task (Mission Control's "Let Claude handle it")
|
||||||
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
// has no WorktreeEntity, so its review range falls back to HandlerBaseCommit/
|
||||||
|
// HandlerHeadCommit -- see TaskEntity for why.
|
||||||
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
|
||||||
|
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
||||||
@@ -740,6 +847,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
WorktreeStateLabel, _listWorkingDir);
|
WorktreeStateLabel, _listWorkingDir);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
|
OnPropertyChanged(nameof(CanSubmitForReview));
|
||||||
|
SubmitForReviewCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnWorktreeHeadCommitChanged(string? value) =>
|
partial void OnWorktreeHeadCommitChanged(string? value) =>
|
||||||
@@ -748,6 +857,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
partial void OnTaskChanged(TaskRowViewModel? value)
|
partial void OnTaskChanged(TaskRowViewModel? value)
|
||||||
{
|
{
|
||||||
|
ReviewDiffViewed = false;
|
||||||
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
@@ -933,6 +1043,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private bool CanContinue() =>
|
private bool CanContinue() =>
|
||||||
Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);
|
Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSendRoadblockReply))]
|
||||||
|
private async System.Threading.Tasks.Task SendRoadblockReplyAsync()
|
||||||
|
{
|
||||||
|
if (Task == null) return;
|
||||||
|
var text = RoadblockReplyDraft;
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _worker.ContinueTaskAsync(Task.Id, text);
|
||||||
|
RoadblockReplyDraft = string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("details.roadblockReply.failed", ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanSendRoadblockReply() =>
|
||||||
|
Task != null && _worker.IsConnected && CanReplyToRoadblock && !string.IsNullOrWhiteSpace(RoadblockReplyDraft);
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanResetAndRetry))]
|
[RelayCommand(CanExecute = nameof(CanResetAndRetry))]
|
||||||
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
||||||
{
|
{
|
||||||
@@ -958,7 +1088,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private bool CanResetAndRetry() =>
|
private bool CanResetAndRetry() =>
|
||||||
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
||||||
|
|
||||||
[RelayCommand]
|
// Set once the user opens the diff/combined-diff for the current review. Reset on
|
||||||
|
// task switch and on every state change (a new run means a new diff to read), so
|
||||||
|
// Approve & Merge stays blocked until the pending changes have been inspected.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ApproveReviewCommand))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowReviewDiffHint))]
|
||||||
|
private bool _reviewDiffViewed;
|
||||||
|
|
||||||
|
// True while a review is pending, there is a diff to inspect, and it has not been
|
||||||
|
// opened yet — drives the "open the diff first" hint next to Approve & Merge.
|
||||||
|
public bool ShowReviewDiffHint =>
|
||||||
|
IsWaitingForReview && Merge.HasReviewableDiff && !ReviewDiffViewed;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanApproveReview))]
|
||||||
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||||
{
|
{
|
||||||
if (Task is null || !_worker.IsConnected) return;
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
@@ -968,6 +1111,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
if (!hasChildren && result?.Status == "conflict")
|
if (!hasChildren && result?.Status == "conflict")
|
||||||
await _merge.ResolveConflictAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
await _merge.ResolveConflictAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
|
// The merge itself already landed; the verify command failed, so the task stayed
|
||||||
|
// out of Done. Surface that instead of silently looking like nothing happened.
|
||||||
|
else if (!hasChildren && result?.Status == "verify_failed" && ShowErrorAsync != null)
|
||||||
|
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -976,6 +1123,30 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force the diff to have been opened before a merge can happen — but only when
|
||||||
|
// there is actually something to inspect (a childless sandbox run with no worktree
|
||||||
|
// has no diff, so it approves straight through).
|
||||||
|
private bool CanApproveReview() =>
|
||||||
|
Task != null && _worker.IsConnected && IsWaitingForReview
|
||||||
|
&& (!Merge.HasReviewableDiff || ReviewDiffViewed);
|
||||||
|
|
||||||
|
// An interactive (ConPTY) session leaves its worktree changed but never flips the task
|
||||||
|
// status. Offer "Submit for review" for an Idle/Failed task that still has a worktree, so
|
||||||
|
// its hand-driven work can enter the normal review/merge flow (commits first, server-side).
|
||||||
|
public bool CanSubmitForReview =>
|
||||||
|
Task != null && _worker.IsConnected && (IsIdle || IsFailed) && !string.IsNullOrEmpty(WorktreePath);
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSubmitForReview))]
|
||||||
|
private async System.Threading.Tasks.Task SubmitForReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
try { await _worker.SubmitTaskForReviewAsync(Task.Id); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(HasReviewFeedback))]
|
[RelayCommand(CanExecute = nameof(HasReviewFeedback))]
|
||||||
private async System.Threading.Tasks.Task RejectReviewAsync()
|
private async System.Threading.Tasks.Task RejectReviewAsync()
|
||||||
{
|
{
|
||||||
@@ -1080,7 +1251,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failures.Add($"{fileName}: {ex.Message}");
|
// Keep the exception type in the message so an intermittent failure is analyzable.
|
||||||
|
failures.Add($"{fileName}: {ex.GetType().Name}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ 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;
|
||||||
|
// Reminder list: tasks created here default to manual.
|
||||||
|
[ObservableProperty] private bool _isManual;
|
||||||
[ObservableProperty] private bool _dropHintAbove;
|
[ObservableProperty] private bool _dropHintAbove;
|
||||||
[ObservableProperty] private bool _dropHintBelow;
|
[ObservableProperty] private bool _dropHintBelow;
|
||||||
|
// Set while a dragged task hovers over this row as a move target — distinct from
|
||||||
|
// DropHintAbove/Below, which mark an insertion point for list reordering.
|
||||||
|
[ObservableProperty] private bool _isTaskDropTarget;
|
||||||
public string? IconKey { get; init; }
|
public string? IconKey { get; init; }
|
||||||
public string? DotColorKey { get; init; }
|
public string? DotColorKey { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
|||||||
|
|
||||||
public enum ListKind { Smart, Virtual, User }
|
public enum ListKind { Smart, Virtual, User }
|
||||||
|
|
||||||
|
/// <summary>Confirmed handler run: the scope list and the ordered selected task ids.</summary>
|
||||||
|
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds);
|
||||||
|
|
||||||
public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
@@ -44,7 +47,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
if (row is null || Dialogs is null || _services is null) return;
|
if (row is null || Dialogs is null || _services is null) return;
|
||||||
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType);
|
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType, row.IsManual);
|
||||||
await Dialogs.ShowListSettingsAsync(vm);
|
await Dialogs.ShowListSettingsAsync(vm);
|
||||||
if (vm.Deleted) await LoadAsync();
|
if (vm.Deleted) await LoadAsync();
|
||||||
else await RefreshRowAsync(row.Id);
|
else await RefreshRowAsync(row.Id);
|
||||||
@@ -80,6 +83,23 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
finally { _worktreesOverviewOpen = false; }
|
finally { _worktreesOverviewOpen = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Raised after the merge-helper selection dialog is confirmed; the shell opens the ConPTY tile.</summary>
|
||||||
|
public event Action<MergeHelperRequest>? LetClaudeHandleRequested;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task LetClaudeHandleListAsync(ListNavItemViewModel? row)
|
||||||
|
{
|
||||||
|
if (row is null || Dialogs is null || _services is null) return;
|
||||||
|
if (row.Kind != ListKind.User || string.IsNullOrWhiteSpace(row.WorkingDir)) return;
|
||||||
|
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||||
|
var vm = _services.GetRequiredService<MergeHelperSelectionModalViewModel>();
|
||||||
|
vm.Configure(rawId, row.Name);
|
||||||
|
await vm.LoadAsync();
|
||||||
|
var ids = await Dialogs.ShowMergeHelperSelectionAsync(vm);
|
||||||
|
if (ids is { Count: > 0 })
|
||||||
|
LetClaudeHandleRequested?.Invoke(new MergeHelperRequest(rawId, ids));
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenInExplorer(ListNavItemViewModel? row)
|
private void OpenInExplorer(ListNavItemViewModel? row)
|
||||||
{
|
{
|
||||||
@@ -223,6 +243,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
DotColorKey = dotColors[idx % dotColors.Length],
|
DotColorKey = dotColors[idx % dotColors.Length],
|
||||||
WorkingDir = l.WorkingDir,
|
WorkingDir = l.WorkingDir,
|
||||||
DefaultCommitType = l.DefaultCommitType,
|
DefaultCommitType = l.DefaultCommitType,
|
||||||
|
IsManual = l.IsManual,
|
||||||
};
|
};
|
||||||
Items.Add(item);
|
Items.Add(item);
|
||||||
UserLists.Add(item);
|
UserLists.Add(item);
|
||||||
@@ -297,7 +318,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
if (Dialogs is not null && _services is not null)
|
if (Dialogs is not null && _services is not null)
|
||||||
{
|
{
|
||||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType);
|
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType, entity.IsManual);
|
||||||
await Dialogs.ShowListSettingsAsync(vm);
|
await Dialogs.ShowListSettingsAsync(vm);
|
||||||
if (vm.Deleted) await LoadAsync();
|
if (vm.Deleted) await LoadAsync();
|
||||||
else await RefreshRowAsync(item.Id);
|
else await RefreshRowAsync(item.Id);
|
||||||
@@ -377,6 +398,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
row.Name = entity.Name;
|
row.Name = entity.Name;
|
||||||
row.WorkingDir = entity.WorkingDir;
|
row.WorkingDir = entity.WorkingDir;
|
||||||
row.DefaultCommitType = entity.DefaultCommitType;
|
row.DefaultCommitType = entity.DefaultCommitType;
|
||||||
|
row.IsManual = entity.IsManual;
|
||||||
}
|
}
|
||||||
catch { /* best-effort refresh */ }
|
catch { /* best-effort refresh */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,24 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
public bool ShowMergePreviewMuted =>
|
public bool ShowMergePreviewMuted =>
|
||||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||||
|
|
||||||
|
// CanDiffMergedRange covers a worktree-less list-handler host task (no live worktree, but
|
||||||
|
// a HandlerBaseCommit/HandlerHeadCommit review range over the list's working dir) so its
|
||||||
|
// merge/diff card still renders even though _worktreePath stays null for it.
|
||||||
public bool ShowMergeSection =>
|
public bool ShowMergeSection =>
|
||||||
_worktreePath != null || _isPlanningParent || _hasChildOutcomes;
|
_worktreePath != null || _isPlanningParent || _hasChildOutcomes || CanDiffMergedRange;
|
||||||
|
|
||||||
public Func<DiffViewerViewModel, System.Threading.Tasks.Task>? ShowDiffViewer { get; set; }
|
public Func<DiffViewerViewModel, System.Threading.Tasks.Task>? ShowDiffViewer { get; set; }
|
||||||
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
||||||
|
|
||||||
|
// Raised when the user opens a diff/combined-diff for the current task, so the
|
||||||
|
// review gate can record that the changes were inspected before merging.
|
||||||
|
public Action? DiffViewed { get; set; }
|
||||||
|
|
||||||
|
// True when there is something to inspect before merging (a live worktree diff,
|
||||||
|
// a merged commit range, or a planning/children combined diff). When false there
|
||||||
|
// is nothing to read, so the review gate must not block approve.
|
||||||
|
public bool HasReviewableDiff => CanOpenDiff() || CanReviewDiff();
|
||||||
|
|
||||||
public MergeSectionViewModel(IWorkerClient worker, IServiceProvider services)
|
public MergeSectionViewModel(IWorkerClient worker, IServiceProvider services)
|
||||||
{
|
{
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
@@ -69,6 +81,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
_worktreeStateLabel = worktreeState;
|
_worktreeStateLabel = worktreeState;
|
||||||
_listWorkingDir = listWorkDir;
|
_listWorkingDir = listWorkDir;
|
||||||
OnPropertyChanged(nameof(ShowMergeSection));
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
OnPropertyChanged(nameof(HasReviewableDiff));
|
||||||
OpenDiffCommand.NotifyCanExecuteChanged();
|
OpenDiffCommand.NotifyCanExecuteChanged();
|
||||||
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
@@ -79,6 +92,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
TaskTitle = taskTitle;
|
TaskTitle = taskTitle;
|
||||||
_isPlanningParent = isPlanningParent;
|
_isPlanningParent = isPlanningParent;
|
||||||
OnPropertyChanged(nameof(ShowMergeSection));
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
OnPropertyChanged(nameof(HasReviewableDiff));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SyncChildOutcomes(bool hasChildOutcomes, int subtaskCount)
|
internal void SyncChildOutcomes(bool hasChildOutcomes, int subtaskCount)
|
||||||
@@ -86,6 +100,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
_hasChildOutcomes = hasChildOutcomes;
|
_hasChildOutcomes = hasChildOutcomes;
|
||||||
_subtaskCount = subtaskCount;
|
_subtaskCount = subtaskCount;
|
||||||
OnPropertyChanged(nameof(ShowMergeSection));
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
OnPropertyChanged(nameof(HasReviewableDiff));
|
||||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +143,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
var vm = _services.GetRequiredService<DiffViewerViewModel>();
|
var vm = _services.GetRequiredService<DiffViewerViewModel>();
|
||||||
vm.ConfigurePlanning(TaskId, SelectedMergeTarget ?? "main");
|
vm.ConfigurePlanning(TaskId, SelectedMergeTarget ?? "main");
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
DiffViewed?.Invoke();
|
||||||
await ShowDiffViewer(vm);
|
await ShowDiffViewer(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +173,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
else return;
|
else return;
|
||||||
|
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
|
DiffViewed?.Invoke();
|
||||||
await ShowDiffViewer(vm);
|
await ShowDiffViewer(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
|||||||
|
|
||||||
public NotesEditorViewModel(INotesApi api) => _api = api;
|
public NotesEditorViewModel(INotesApi api) => _api = api;
|
||||||
|
|
||||||
|
// Raised when a worker call fails so the host VM can surface it (e.g. via ShowErrorAsync).
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
||||||
@@ -55,10 +58,17 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
var text = NewBulletText.Trim();
|
var text = NewBulletText.Trim();
|
||||||
if (text.Length == 0) return;
|
if (text.Length == 0) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
var dto = await _api.AddAsync(CurrentDay, text);
|
var dto = await _api.AddAsync(CurrentDay, text);
|
||||||
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||||
NewBulletText = "";
|
NewBulletText = "";
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
||||||
[RelayCommand] private Task NextDay() => LoadDayAsync(CurrentDay.AddDays(1));
|
[RelayCommand] private Task NextDay() => LoadDayAsync(CurrentDay.AddDays(1));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
@@ -26,13 +27,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty] private string _agentState = "idle";
|
[ObservableProperty] private string _agentState = "idle";
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
|
|
||||||
private string? _title;
|
|
||||||
|
|
||||||
public string DisplayTitle =>
|
|
||||||
string.IsNullOrWhiteSpace(Title) ? (SubscribedTaskId ?? "task") : Title!;
|
|
||||||
|
|
||||||
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
public bool IsIdle => AgentState == "idle";
|
public bool IsIdle => AgentState == "idle";
|
||||||
public bool IsQueued => AgentState == "queued";
|
public bool IsQueued => AgentState == "queued";
|
||||||
@@ -70,9 +64,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||||
|
|
||||||
public ObservableCollection<QueuedMessageViewModel> QueuedMessages { get; } = new();
|
|
||||||
public bool HasQueuedMessages => QueuedMessages.Count > 0;
|
|
||||||
|
|
||||||
// Captured handler delegates for disposal
|
// Captured handler delegates for disposal
|
||||||
private readonly Action<string, string> _onTaskMessage;
|
private readonly Action<string, string> _onTaskMessage;
|
||||||
private readonly Action<string, string, DateTime> _onTaskStarted;
|
private readonly Action<string, string, DateTime> _onTaskStarted;
|
||||||
@@ -80,19 +71,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Action<string> _onTaskUpdated;
|
private readonly Action<string> _onTaskUpdated;
|
||||||
private readonly Action<string, string, string> _onTaskQuestionAsked;
|
private readonly Action<string, string, string> _onTaskQuestionAsked;
|
||||||
private readonly Action<string, string> _onTaskQuestionResolved;
|
private readonly Action<string, string> _onTaskQuestionResolved;
|
||||||
private readonly Action<string> _onInteractiveStarted;
|
|
||||||
private readonly Action<string> _onInteractiveEnded;
|
|
||||||
private readonly Action<string, IReadOnlyList<string>> _onInteractiveQueueChanged;
|
|
||||||
private readonly Action<string, string> _onInteractiveMessageSent;
|
|
||||||
|
|
||||||
// Interactive composer — active while the worker is in an interactive session.
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(SubmitComposerCommand))]
|
|
||||||
private bool _isInteractiveLive;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(SubmitComposerCommand))]
|
|
||||||
private string _composerDraft = string.Empty;
|
|
||||||
|
|
||||||
// A question the running task raised via AskUser and is blocking on, plus the answer
|
// A question the running task raised via AskUser and is blocking on, plus the answer
|
||||||
// the user is typing. Ephemeral (in-memory + live events) — the task is still Running.
|
// the user is typing. Ephemeral (in-memory + live events) — the task is still Running.
|
||||||
@@ -160,46 +138,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
ClearPendingQuestion();
|
ClearPendingQuestion();
|
||||||
};
|
};
|
||||||
_worker.TaskQuestionResolvedEvent += _onTaskQuestionResolved;
|
_worker.TaskQuestionResolvedEvent += _onTaskQuestionResolved;
|
||||||
|
|
||||||
_onInteractiveStarted = taskId =>
|
|
||||||
{
|
|
||||||
if (taskId == _subscribedTaskId) { IsInteractiveLive = true; AgentState = "running"; }
|
|
||||||
};
|
|
||||||
_worker.InteractiveSessionStartedEvent += _onInteractiveStarted;
|
|
||||||
|
|
||||||
_onInteractiveEnded = taskId =>
|
|
||||||
{
|
|
||||||
if (taskId != _subscribedTaskId) return;
|
|
||||||
IsInteractiveLive = false;
|
|
||||||
AgentState = "done";
|
|
||||||
QueuedMessages.Clear();
|
|
||||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
|
||||||
};
|
|
||||||
_worker.InteractiveSessionEndedEvent += _onInteractiveEnded;
|
|
||||||
|
|
||||||
_onInteractiveQueueChanged = (taskId, pending) =>
|
|
||||||
{
|
|
||||||
if (taskId != _subscribedTaskId) return;
|
|
||||||
QueuedMessages.Clear();
|
|
||||||
foreach (var m in pending)
|
|
||||||
{
|
|
||||||
var text = m;
|
|
||||||
QueuedMessages.Add(new QueuedMessageViewModel
|
|
||||||
{
|
|
||||||
Text = text,
|
|
||||||
RemoveCommand = new CommunityToolkit.Mvvm.Input.RelayCommand(() => _ = RemoveQueuedAsync(text)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
|
||||||
};
|
|
||||||
_worker.InteractiveQueueChangedEvent += _onInteractiveQueueChanged;
|
|
||||||
|
|
||||||
_onInteractiveMessageSent = (taskId, text) =>
|
|
||||||
{
|
|
||||||
if (taskId == _subscribedTaskId)
|
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.User, Text = text });
|
|
||||||
};
|
|
||||||
_worker.InteractiveMessageSentEvent += _onInteractiveMessageSent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Surface a pending question (used by live event + re-attach hydration).
|
// Surface a pending question (used by live event + re-attach hydration).
|
||||||
@@ -209,45 +147,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
PendingQuestion = question;
|
PendingQuestion = question;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used by Mission Control when it creates the monitor after the started event already fired.
|
|
||||||
public void SetInteractiveLive(bool live)
|
|
||||||
{
|
|
||||||
IsInteractiveLive = live;
|
|
||||||
if (live) AgentState = "running";
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanSubmitComposer))]
|
|
||||||
private async System.Threading.Tasks.Task SubmitComposer()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(_subscribedTaskId)) return;
|
|
||||||
var text = ComposerDraft;
|
|
||||||
if (string.IsNullOrWhiteSpace(text)) return;
|
|
||||||
ComposerDraft = string.Empty;
|
|
||||||
await _worker.SendInteractiveMessageAsync(_subscribedTaskId, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanSubmitComposer() => IsInteractiveLive && !string.IsNullOrWhiteSpace(ComposerDraft);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task StopInteractive()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && IsInteractiveLive)
|
|
||||||
await _worker.StopInteractiveSessionAsync(_subscribedTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task InterruptInteractive()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && IsInteractiveLive)
|
|
||||||
await _worker.InterruptInteractiveSessionAsync(_subscribedTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RemoveQueuedAsync(string text)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId))
|
|
||||||
await _worker.RemoveQueuedInteractiveMessageAsync(_subscribedTaskId, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearPendingQuestion()
|
private void ClearPendingQuestion()
|
||||||
{
|
{
|
||||||
PendingQuestionId = null;
|
PendingQuestionId = null;
|
||||||
@@ -296,47 +195,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
SessionOutcome = null;
|
SessionOutcome = null;
|
||||||
Roadblocks = null;
|
Roadblocks = null;
|
||||||
ClearPendingQuestion();
|
ClearPendingQuestion();
|
||||||
IsInteractiveLive = false;
|
|
||||||
ComposerDraft = string.Empty;
|
|
||||||
QueuedMessages.Clear();
|
|
||||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
|
||||||
}
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(DetachTooltip))]
|
|
||||||
private bool _isDetached;
|
|
||||||
|
|
||||||
// Localized tooltip for the detach/re-dock toggle button.
|
|
||||||
public string DetachTooltip => Loc.T(IsDetached ? "missionControl.redock" : "missionControl.detach");
|
|
||||||
|
|
||||||
// Set by the detached window so the re-dock action can close it.
|
|
||||||
public Action? CloseWindowRequested { get; set; }
|
|
||||||
|
|
||||||
// Set by the host (e.g. Mission Control) to navigate the main app to this task.
|
|
||||||
public Action<string>? OpenInAppRequested { get; set; }
|
|
||||||
|
|
||||||
// Set by the host (Mission Control) to pop this monitor out into its own window.
|
|
||||||
public Action<TaskMonitorViewModel>? DetachRequested { get; set; }
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Detach()
|
|
||||||
{
|
|
||||||
if (IsDetached) CloseWindowRequested?.Invoke(); // re-dock: close the detached window
|
|
||||||
else DetachRequested?.Invoke(this); // detach: pop out to its own window
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenInApp()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId))
|
|
||||||
OpenInAppRequested?.Invoke(_subscribedTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task CancelTask()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && (IsRunning || IsQueued))
|
|
||||||
await _worker.CancelTaskAsync(_subscribedTaskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTaskId(string id) => _subscribedTaskId = id;
|
public void SetTaskId(string id) => _subscribedTaskId = id;
|
||||||
@@ -356,16 +214,38 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal);
|
var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal);
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
{
|
{
|
||||||
SessionOutcome = result;
|
SessionOutcome = UnwrapSummary(result);
|
||||||
Roadblocks = null;
|
Roadblocks = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd();
|
var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd();
|
||||||
SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary;
|
SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : UnwrapSummary(summary);
|
||||||
Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim();
|
Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Safety net for older rows persisted as raw structured-output JSON: if the outcome
|
||||||
|
/// is a JSON object with a string <c>summary</c>, show that; otherwise keep the text as-is.</summary>
|
||||||
|
private static string? UnwrapSummary(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return text;
|
||||||
|
var trimmed = text.TrimStart();
|
||||||
|
if (trimmed.Length == 0 || trimmed[0] != '{') return text;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(trimmed);
|
||||||
|
if (doc.RootElement.ValueKind == JsonValueKind.Object &&
|
||||||
|
doc.RootElement.TryGetProperty("summary", out var s) &&
|
||||||
|
s.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var summary = s.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(summary)) return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
public async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
public async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(logPath)) return;
|
if (string.IsNullOrWhiteSpace(logPath)) return;
|
||||||
@@ -523,15 +403,5 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||||
_worker.TaskQuestionAskedEvent -= _onTaskQuestionAsked;
|
_worker.TaskQuestionAskedEvent -= _onTaskQuestionAsked;
|
||||||
_worker.TaskQuestionResolvedEvent -= _onTaskQuestionResolved;
|
_worker.TaskQuestionResolvedEvent -= _onTaskQuestionResolved;
|
||||||
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
|
|
||||||
_worker.InteractiveSessionEndedEvent -= _onInteractiveEnded;
|
|
||||||
_worker.InteractiveQueueChangedEvent -= _onInteractiveQueueChanged;
|
|
||||||
_worker.InteractiveMessageSentEvent -= _onInteractiveMessageSent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class QueuedMessageViewModel
|
|
||||||
{
|
|
||||||
public required string Text { get; init; }
|
|
||||||
public required System.Windows.Input.ICommand RemoveCommand { get; init; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,10 +35,20 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _parentInView = true;
|
[ObservableProperty] private bool _parentInView = true;
|
||||||
[ObservableProperty] private int _roadblockCount;
|
[ObservableProperty] private int _roadblockCount;
|
||||||
[ObservableProperty] private bool _isRefining;
|
[ObservableProperty] private bool _isRefining;
|
||||||
|
// Manual = a reminder only the user can do. Every "hand this to Claude" affordance is hidden
|
||||||
|
// and automation skips it; opening a hand-driven ConPTY session stays allowed.
|
||||||
|
[ObservableProperty] private bool _isManual;
|
||||||
|
// Set by the shell from Mission Control's open ConPTY panes: this task has a live hand-driven
|
||||||
|
// session, which outranks the persisted status on the lifecycle chip (an interactive task is
|
||||||
|
// typically Idle+Active-worktree, i.e. would otherwise read "Parked").
|
||||||
|
[ObservableProperty] private bool _hasInteractiveSession;
|
||||||
// Set by the custom drag while this row is being dragged — drives the "grabbed" row style.
|
// Set by the custom drag while this row is being dragged — drives the "grabbed" row style.
|
||||||
[ObservableProperty] private bool _isDragging;
|
[ObservableProperty] private bool _isDragging;
|
||||||
|
|
||||||
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None && !IsRefining;
|
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None
|
||||||
|
&& !IsRefining && !IsManual;
|
||||||
|
|
||||||
|
public string? ManualBadge => IsManual ? Loc.T("tasks.badgeManual") : null;
|
||||||
|
|
||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
||||||
@@ -60,7 +70,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public bool CanOpenPlanningSession => Status == TaskStatus.Idle
|
public bool CanOpenPlanningSession => Status == TaskStatus.Idle
|
||||||
&& PlanningPhase == PlanningPhase.None
|
&& PlanningPhase == PlanningPhase.None
|
||||||
&& !IsChild;
|
&& !IsChild
|
||||||
|
&& !IsManual;
|
||||||
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
||||||
|
|
||||||
public string? PlanningBadge => PlanningPhase switch
|
public string? PlanningBadge => PlanningPhase switch
|
||||||
@@ -79,8 +90,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done;
|
public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done;
|
||||||
public bool IsRunning => Status == TaskStatus.Running;
|
public bool IsRunning => Status == TaskStatus.Running;
|
||||||
public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;
|
public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;
|
||||||
// Parked = set aside from review: Idle but still holding its Active worktree (vs a plain Idle task).
|
// Parked = set aside from review: Idle but still holding its Active worktree (vs a plain Idle
|
||||||
public bool IsParked => Status == TaskStatus.Idle && WorktreeState == ClaudeDo.Data.Models.WorktreeState.Active;
|
// task). A live ConPTY session is that same shape but not parked — it reads "Interactive".
|
||||||
|
public bool IsParked => Status == TaskStatus.Idle && WorktreeState == ClaudeDo.Data.Models.WorktreeState.Active
|
||||||
|
&& !HasInteractiveSession;
|
||||||
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
|
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
|
||||||
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
|
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
|
||||||
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
|
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
|
||||||
@@ -89,7 +102,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
// it must be finalized first.
|
// it must be finalized first.
|
||||||
public bool CanSendToQueue => !IsRunning && !IsQueued && !IsWaitingForReview && !HasQueuedSubtasks
|
public bool CanSendToQueue => !IsRunning && !IsQueued && !IsWaitingForReview && !HasQueuedSubtasks
|
||||||
&& (!IsChild || ParentFinalized)
|
&& (!IsChild || ParentFinalized)
|
||||||
&& PlanningPhase != PlanningPhase.Active;
|
&& PlanningPhase != PlanningPhase.Active
|
||||||
|
&& !IsManual;
|
||||||
// Parent-level "send plan to queue" — only once the plan is finalized (children Planned).
|
// Parent-level "send plan to queue" — only once the plan is finalized (children Planned).
|
||||||
// Drives the routing inside SendToQueue, not a separate menu entry.
|
// Drives the routing inside SendToQueue, not a separate menu entry.
|
||||||
public bool CanQueuePlan => !IsChild && HasPlanningChildren
|
public bool CanQueuePlan => !IsChild && HasPlanningChildren
|
||||||
@@ -110,7 +124,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||||
public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount);
|
public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount);
|
||||||
|
|
||||||
public string StatusLabel => IsParked ? Loc.T("vm.taskStatus.parked") : Status switch
|
// Null on a plain status chip so no tooltip shows there — the chip is only actionable
|
||||||
|
// while a session is open.
|
||||||
|
public string? InteractiveChipTooltip
|
||||||
|
=> HasInteractiveSession ? Loc.T("tasks.interactiveChipTip") : null;
|
||||||
|
|
||||||
|
public string StatusLabel
|
||||||
|
=> HasInteractiveSession ? Loc.T("vm.taskStatus.interactive")
|
||||||
|
: IsParked ? Loc.T("vm.taskStatus.parked") : Status switch
|
||||||
{
|
{
|
||||||
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
|
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
|
||||||
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
||||||
@@ -123,7 +144,16 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
_ => Status.ToString(),
|
_ => Status.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
|
// A planning parent that is still Idle (Active planning, or Finalized-but-not-yet-queued)
|
||||||
|
// already shows its state via the PLANNING/PLANNED badge next to the title. The lifecycle
|
||||||
|
// chip would read "Idle" (or "Parked"), which is misleading, so hide it in that case —
|
||||||
|
// unless there's a live session, where "Interactive" is exactly what the user needs to see.
|
||||||
|
public bool ShowStatusChip => HasInteractiveSession
|
||||||
|
|| !(PlanningPhase != PlanningPhase.None && Status == TaskStatus.Idle);
|
||||||
|
|
||||||
|
public string StatusChipClass => HasInteractiveSession
|
||||||
|
? "interactive"
|
||||||
|
: (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
|
||||||
{
|
{
|
||||||
(TaskStatus.Running, _) => "running",
|
(TaskStatus.Running, _) => "running",
|
||||||
(TaskStatus.WaitingForReview, _) => "review",
|
(TaskStatus.WaitingForReview, _) => "review",
|
||||||
@@ -139,6 +169,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(StatusChipClass));
|
OnPropertyChanged(nameof(StatusChipClass));
|
||||||
OnPropertyChanged(nameof(StatusLabel));
|
OnPropertyChanged(nameof(StatusLabel));
|
||||||
|
OnPropertyChanged(nameof(ShowStatusChip));
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
OnPropertyChanged(nameof(IsWaitingForReview));
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
OnPropertyChanged(nameof(IsParked));
|
OnPropertyChanged(nameof(IsParked));
|
||||||
@@ -183,6 +214,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsPlanningParent));
|
OnPropertyChanged(nameof(IsPlanningParent));
|
||||||
OnPropertyChanged(nameof(PlanningBadge));
|
OnPropertyChanged(nameof(PlanningBadge));
|
||||||
|
OnPropertyChanged(nameof(ShowStatusChip));
|
||||||
OnPropertyChanged(nameof(IsPlanActive));
|
OnPropertyChanged(nameof(IsPlanActive));
|
||||||
OnPropertyChanged(nameof(IsPlanFinalized));
|
OnPropertyChanged(nameof(IsPlanFinalized));
|
||||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||||
@@ -195,6 +227,23 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
|
|
||||||
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
|
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
|
||||||
|
|
||||||
|
partial void OnIsManualChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ManualBadge));
|
||||||
|
OnPropertyChanged(nameof(CanRefine));
|
||||||
|
OnPropertyChanged(nameof(CanSendToQueue));
|
||||||
|
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnHasInteractiveSessionChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsParked));
|
||||||
|
OnPropertyChanged(nameof(StatusChipClass));
|
||||||
|
OnPropertyChanged(nameof(StatusLabel));
|
||||||
|
OnPropertyChanged(nameof(ShowStatusChip));
|
||||||
|
OnPropertyChanged(nameof(InteractiveChipTooltip));
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnHasQueuedSubtasksChanged(bool value)
|
partial void OnHasQueuedSubtasksChanged(bool value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||||
@@ -259,6 +308,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
Done = t.Status == TaskStatus.Done;
|
Done = t.Status == TaskStatus.Done;
|
||||||
IsStarred = t.IsStarred;
|
IsStarred = t.IsStarred;
|
||||||
IsMyDay = t.IsMyDay;
|
IsMyDay = t.IsMyDay;
|
||||||
|
IsManual = t.IsManual;
|
||||||
Status = t.Status;
|
Status = t.Status;
|
||||||
PlanningPhase = t.PlanningPhase;
|
PlanningPhase = t.PlanningPhase;
|
||||||
Branch = t.Worktree?.BranchName;
|
Branch = t.Worktree?.BranchName;
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Dictionary<string, bool> _expandedState = new();
|
private readonly Dictionary<string, bool> _expandedState = new();
|
||||||
private ListNavItemViewModel? _currentList;
|
private ListNavItemViewModel? _currentList;
|
||||||
private CancellationTokenSource? _loadCts;
|
private CancellationTokenSource? _loadCts;
|
||||||
|
// Task ids with a live ConPTY pane in Mission Control; kept here so rows loaded later still
|
||||||
|
// pick the flag up (see SyncInteractiveSessions).
|
||||||
|
private readonly HashSet<string> _interactiveSessionIds = new();
|
||||||
private static readonly TaskListFilterRegistry _filters = new();
|
private static readonly TaskListFilterRegistry _filters = new();
|
||||||
|
|
||||||
public event EventHandler? SelectionChanged;
|
public event EventHandler? SelectionChanged;
|
||||||
@@ -69,6 +72,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[ObservableProperty] private string _completedHeader = "";
|
[ObservableProperty] private string _completedHeader = "";
|
||||||
[ObservableProperty] private bool _showNotesRow;
|
[ObservableProperty] private bool _showNotesRow;
|
||||||
[ObservableProperty] private bool _isMyDayList;
|
[ObservableProperty] private bool _isMyDayList;
|
||||||
|
[ObservableProperty] private bool _isLetClaudeVisible;
|
||||||
|
|
||||||
|
public event EventHandler? LetClaudeHandleRequested;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void LetClaudeHandle() => LetClaudeHandleRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
internal Task? LoadTask { get; private set; }
|
internal Task? LoadTask { get; private set; }
|
||||||
|
|
||||||
@@ -151,6 +160,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
|
// A parent transition (finalize/discard) broadcasts only the parent's id, but it
|
||||||
|
// changes its children's derived state — finalize flips them Draft→Planned, discard
|
||||||
|
// deletes them. The delta path below only touches the parent row and never recomputes
|
||||||
|
// the child-derived flags (ParentFinalized, HasPlanningChildren) nor drops deleted
|
||||||
|
// children, so reconcile the whole list when the updated task is (or owns) a subtree.
|
||||||
|
if (entity is not null &&
|
||||||
|
(entity.PlanningPhase != PlanningPhase.None || Items.Any(r => r.ParentTaskId == entity.Id)))
|
||||||
|
{
|
||||||
|
LoadForList(list);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
|
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
@@ -196,8 +217,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private void OnCurrentListPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void OnCurrentListPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(ListNavItemViewModel.Name) && sender is ListNavItemViewModel vm)
|
if (sender is not ListNavItemViewModel vm) return;
|
||||||
|
if (e.PropertyName == nameof(ListNavItemViewModel.Name))
|
||||||
HeaderTitle = vm.Name;
|
HeaderTitle = vm.Name;
|
||||||
|
else if (e.PropertyName == nameof(ListNavItemViewModel.WorkingDir))
|
||||||
|
IsLetClaudeVisible = vm.Kind == ListKind.User && !string.IsNullOrWhiteSpace(vm.WorkingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadForList(ListNavItemViewModel? list)
|
public void LoadForList(ListNavItemViewModel? list)
|
||||||
@@ -207,6 +231,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_loadCts = new CancellationTokenSource();
|
_loadCts = new CancellationTokenSource();
|
||||||
var ct = _loadCts.Token;
|
var ct = _loadCts.Token;
|
||||||
|
|
||||||
|
// Items is rebuilt from scratch below, so a selection carried over from the previous list
|
||||||
|
// would leave the detail pane bound to a task the visible list no longer contains. Only a
|
||||||
|
// *different* list drops it — a reload of the same list (worker refresh, reconnect) keeps
|
||||||
|
// the selection so a live update never yanks the detail pane away.
|
||||||
|
var listChanged = !string.Equals(_currentList?.Id, list?.Id, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// A same-list reload (triggered by OnWorkerTaskUpdated's full-reload branches) rebuilds
|
||||||
|
// every row from scratch. Reusing the previous instances by id — instead of handing back
|
||||||
|
// brand new ones — keeps SelectedTask (and the bound DetailsIslandViewModel.Task) pointed
|
||||||
|
// at a live row instead of an orphan that never receives another update.
|
||||||
|
var reusable = listChanged ? null : Items.ToDictionary(r => r.Id);
|
||||||
|
|
||||||
if (_currentList is not null)
|
if (_currentList is not null)
|
||||||
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
||||||
_currentList = list;
|
_currentList = list;
|
||||||
@@ -222,17 +258,20 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
HasCompleted = false;
|
HasCompleted = false;
|
||||||
ShowOpenLabel = false;
|
ShowOpenLabel = false;
|
||||||
ShowNotesRow = false;
|
ShowNotesRow = false;
|
||||||
if (list is null) { LoadTask = Task.CompletedTask; return; }
|
if (listChanged) SelectedTask = null;
|
||||||
|
if (list is null) { IsLetClaudeVisible = false; LoadTask = Task.CompletedTask; return; }
|
||||||
|
|
||||||
HeaderTitle = list.Name;
|
HeaderTitle = list.Name;
|
||||||
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
|
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
|
||||||
ShowNotesRow = list.Id == "smart:my-day";
|
ShowNotesRow = list.Id == "smart:my-day";
|
||||||
IsMyDayList = list.Id == "smart:my-day";
|
IsMyDayList = list.Id == "smart:my-day";
|
||||||
|
IsLetClaudeVisible = list.Kind == ListKind.User && !string.IsNullOrWhiteSpace(list.WorkingDir);
|
||||||
|
|
||||||
LoadTask = LoadForListAsync(list, ct);
|
LoadTask = LoadForListAsync(list, ct, reusable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadForListAsync(ListNavItemViewModel list, CancellationToken ct)
|
private async Task LoadForListAsync(
|
||||||
|
ListNavItemViewModel list, CancellationToken ct, Dictionary<string, TaskRowViewModel>? reusable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -260,8 +299,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var showListChip = list.Kind == ListKind.Virtual;
|
var showListChip = list.Kind == ListKind.Virtual;
|
||||||
foreach (var t in filteredList)
|
foreach (var t in filteredList)
|
||||||
{
|
{
|
||||||
var row = TaskRowViewModel.FromEntity(t);
|
TaskRowViewModel row;
|
||||||
|
if (reusable is not null && reusable.TryGetValue(t.Id, out var existing))
|
||||||
|
{
|
||||||
|
row = existing;
|
||||||
|
row.UpdateFromEntity(t);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row = TaskRowViewModel.FromEntity(t);
|
||||||
|
}
|
||||||
row.ShowListChip = showListChip;
|
row.ShowListChip = showListChip;
|
||||||
|
row.HasInteractiveSession = _interactiveSessionIds.Contains(row.Id);
|
||||||
Items.Add(row);
|
Items.Add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +460,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
|
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
|
||||||
var listId = _currentList.Id["user:".Length..];
|
var listId = _currentList.Id["user:".Length..];
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
// A manual list holds reminders, so tasks created in it start out manual.
|
||||||
|
var listIsManual = await db.Lists.Where(l => l.Id == listId).Select(l => l.IsManual).FirstOrDefaultAsync();
|
||||||
var entity = new TaskEntity
|
var entity = new TaskEntity
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
@@ -418,18 +469,36 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Title = NewTaskTitle.Trim(),
|
Title = NewTaskTitle.Trim(),
|
||||||
Status = TaskStatus.Idle,
|
Status = TaskStatus.Idle,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
IsManual = listIsManual,
|
||||||
};
|
};
|
||||||
await new TaskRepository(db).AddAsync(entity);
|
await new TaskRepository(db).AddAsync(entity);
|
||||||
var row = TaskRowViewModel.FromEntity(entity);
|
var row = TaskRowViewModel.FromEntity(entity);
|
||||||
row.ShowListChip = _currentList?.Kind == ListKind.Virtual;
|
row.ShowListChip = _currentList?.Kind == ListKind.Virtual;
|
||||||
Items.Add(row);
|
Items.Add(row);
|
||||||
Regroup();
|
Regroup();
|
||||||
|
SelectedTask = row;
|
||||||
NewTaskTitle = "";
|
NewTaskTitle = "";
|
||||||
UpdateSubtitle();
|
UpdateSubtitle();
|
||||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Replaces the set of tasks that currently have an open interactive (ConPTY) session,
|
||||||
|
/// so their lifecycle chip reads "Interactive" instead of "Parked".</summary>
|
||||||
|
public void SyncInteractiveSessions(IEnumerable<string> taskIds)
|
||||||
|
{
|
||||||
|
_interactiveSessionIds.Clear();
|
||||||
|
foreach (var id in taskIds)
|
||||||
|
_interactiveSessionIds.Add(id);
|
||||||
|
foreach (var r in Items)
|
||||||
|
r.HasInteractiveSession = _interactiveSessionIds.Contains(r.Id);
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
||||||
|
public string? CurrentListId => _currentList?.Id;
|
||||||
|
|
||||||
|
/// <summary>Set by the shell (mirrors <see cref="ListsIslandViewModel.Dialogs"/>) so a
|
||||||
|
/// cross-repo move can confirm via the shared modal seam.</summary>
|
||||||
|
public IDialogService? Dialogs { get; set; }
|
||||||
|
|
||||||
public void ClearDropHints()
|
public void ClearDropHints()
|
||||||
{
|
{
|
||||||
@@ -532,6 +601,76 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drag-drop move: reassigns a task (and every descendant) to <paramref name="targetList"/>.
|
||||||
|
/// No-op for anything that isn't a user list the task doesn't already sit in.
|
||||||
|
/// Rejects running tasks and tasks (or descendants) holding an Active/Kept worktree — the
|
||||||
|
/// worktree would keep pointing at the source repo. A move across repos without a worktree
|
||||||
|
/// is allowed but asks for confirmation first.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MoveTaskToListAsync(TaskRowViewModel row, ListNavItemViewModel targetList)
|
||||||
|
{
|
||||||
|
if (targetList.Kind != ListKind.User) return;
|
||||||
|
var targetListId = targetList.Id.StartsWith("user:", StringComparison.Ordinal)
|
||||||
|
? targetList.Id["user:".Length..]
|
||||||
|
: targetList.Id;
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
// The row's own list, not _currentList — the task island also shows smart/virtual lists,
|
||||||
|
// whose rows come from many lists and which carry no working dir of their own. Dropping a
|
||||||
|
// task on the list it already sits in is a silent no-op.
|
||||||
|
var sourceListId = await db.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.Id == row.Id).Select(t => t.ListId).FirstOrDefaultAsync();
|
||||||
|
if (sourceListId is null || sourceListId == targetListId) return;
|
||||||
|
|
||||||
|
if (row.IsRunning)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveRunningRejected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo = new TaskRepository(db);
|
||||||
|
|
||||||
|
var descendantIds = await repo.GetDescendantIdsAsync(row.Id);
|
||||||
|
var ownIds = new List<string> { row.Id };
|
||||||
|
ownIds.AddRange(descendantIds);
|
||||||
|
var hasBlockingWorktree = await db.Worktrees.AsNoTracking()
|
||||||
|
.Where(w => ownIds.Contains(w.TaskId)
|
||||||
|
&& (w.State == ClaudeDo.Data.Models.WorktreeState.Active
|
||||||
|
|| w.State == ClaudeDo.Data.Models.WorktreeState.Kept))
|
||||||
|
.AnyAsync();
|
||||||
|
if (hasBlockingWorktree)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveWorktreeRejected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceDir = await db.Lists.AsNoTracking()
|
||||||
|
.Where(l => l.Id == sourceListId).Select(l => l.WorkingDir).FirstOrDefaultAsync();
|
||||||
|
var targetDir = targetList.WorkingDir;
|
||||||
|
var repoChanges = !string.Equals(sourceDir, targetDir, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (repoChanges)
|
||||||
|
{
|
||||||
|
if (Dialogs is null)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveConfirmUnavailable"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ok = await Dialogs.ConfirmAsync(Loc.T("vm.tasksIsland.moveRepoConfirm",
|
||||||
|
string.IsNullOrWhiteSpace(sourceDir) ? "—" : sourceDir,
|
||||||
|
string.IsNullOrWhiteSpace(targetDir) ? "—" : targetDir));
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await repo.MoveToListAsync(row.Id, targetListId);
|
||||||
|
|
||||||
|
Items.Remove(row);
|
||||||
|
Regroup();
|
||||||
|
UpdateSubtitle();
|
||||||
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ToggleDoneAsync(TaskRowViewModel row)
|
private async Task ToggleDoneAsync(TaskRowViewModel row)
|
||||||
{
|
{
|
||||||
@@ -595,6 +734,23 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Flips a task between "Claude can run this" and "manual reminder". A manual task
|
||||||
|
/// hides every hand-off affordance and is skipped by the queue picker and daily prep.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ToggleManualAsync(TaskRowViewModel? row)
|
||||||
|
{
|
||||||
|
if (row is null) return;
|
||||||
|
row.IsManual = !row.IsManual;
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
entity.IsManual = row.IsManual;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task AddToMyDayAsync(TaskRowViewModel? row)
|
private async Task AddToMyDayAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
@@ -732,7 +888,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
if (row is null || !row.IsWaitingForReview || _worker is null) return;
|
if (row is null || !row.IsWaitingForReview || _worker is null) return;
|
||||||
try { await _worker.ApproveReviewAsync(row.Id, ""); }
|
try { await _worker.ApproveReviewAsync(row.Id, ""); }
|
||||||
catch { /* offline; broadcast reconciles on return */ }
|
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.approveFailed", ex.Message)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RejectReviewToQueueAsync(TaskRowViewModel row, string feedback)
|
public async Task RejectReviewToQueueAsync(TaskRowViewModel row, string feedback)
|
||||||
@@ -801,22 +957,29 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private void OpenListSettings() => OpenListSettingsRequested?.Invoke(this, EventArgs.Empty);
|
private void OpenListSettings() => OpenListSettingsRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
|
private void OpenPlanningSession(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null) return;
|
if (row is null) return;
|
||||||
if (row.Status != TaskStatus.Idle || row.PlanningPhase != PlanningPhase.None) return;
|
if (row.Status != TaskStatus.Idle || row.PlanningPhase != PlanningPhase.None) return;
|
||||||
ForegroundHelper.AllowAny();
|
// Planning now runs as an embedded ConPTY pane in the Command Center (not an external wt
|
||||||
try { await _worker!.StartPlanningSessionAsync(row.Id); }
|
// window). The shell owns Mission Control, so raise an event; the actual StartAsync happens
|
||||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.planningOpenFailed", ex.Message)); }
|
// server-side inside GetPlanningStartLaunchSpec when the pane opens.
|
||||||
|
OpenPlanningConPtyRequested?.Invoke(row.Id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opens the task in an embedded ConPTY terminal pane in the Command Center. The shell owns
|
||||||
|
// the Mission Control view model, so this just raises an event for it to act on.
|
||||||
|
public event Action<string>? OpenConPtySessionRequested;
|
||||||
|
|
||||||
|
// Opens (resume=false) or resumes (resume=true) a planning session as an embedded ConPTY
|
||||||
|
// pane in the Command Center.
|
||||||
|
public event Action<string, bool>? OpenPlanningConPtyRequested;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task RunInteractivelyAsync(TaskRowViewModel? row)
|
private void OpenConPtySession(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || _worker is null) return;
|
if (row is null) return;
|
||||||
ForegroundHelper.AllowAny();
|
OpenConPtySessionRequested?.Invoke(row.Id);
|
||||||
try { await _worker.OpenInteractiveTerminalAsync(row.Id); }
|
|
||||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.runInteractiveFailed", ex.Message)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -842,8 +1005,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
switch (choice)
|
switch (choice)
|
||||||
{
|
{
|
||||||
case UnfinishedPlanningModalResult.Resume:
|
case UnfinishedPlanningModalResult.Resume:
|
||||||
ForegroundHelper.AllowAny();
|
// Resume as an embedded ConPTY pane (server-side ResumeAsync runs inside
|
||||||
await _worker.ResumePlanningSessionAsync(row.Id);
|
// GetPlanningResumeLaunchSpec when the pane opens).
|
||||||
|
OpenPlanningConPtyRequested?.Invoke(row.Id, true);
|
||||||
break;
|
break;
|
||||||
case UnfinishedPlanningModalResult.FinalizeNow:
|
case UnfinishedPlanningModalResult.FinalizeNow:
|
||||||
await _worker.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false);
|
await _worker.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false);
|
||||||
@@ -856,7 +1020,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.planningResumeFailed", ex.Message)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
public DetailsIslandViewModel? Details { get; }
|
public DetailsIslandViewModel? Details { get; }
|
||||||
public IWorkerClient? Worker { get; }
|
public IWorkerClient? Worker { get; }
|
||||||
public MissionControlViewModel? MissionControl { get; }
|
public MissionControlViewModel? MissionControl { get; }
|
||||||
|
public UsagePillViewModel? UsagePill { get; }
|
||||||
public UpdateCheckService UpdateCheck => _updateCheck;
|
public UpdateCheckService UpdateCheck => _updateCheck;
|
||||||
|
|
||||||
public string ConnectionText =>
|
public string ConnectionText =>
|
||||||
@@ -36,6 +37,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
||||||
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
||||||
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
||||||
|
private readonly Func<UsageMonitorModalViewModel> _usageMonitorVmFactory = () => null!;
|
||||||
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
|
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
|
||||||
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
|
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
_dialogs = value;
|
_dialogs = value;
|
||||||
if (Lists is not null) Lists.Dialogs = value;
|
if (Lists is not null) Lists.Dialogs = value;
|
||||||
|
if (Tasks is not null) Tasks.Dialogs = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +100,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty] private bool _isUpdateBannerVisible;
|
[ObservableProperty] private bool _isUpdateBannerVisible;
|
||||||
[ObservableProperty] private string? _updateBannerLatestVersion;
|
[ObservableProperty] private string? _updateBannerLatestVersion;
|
||||||
[ObservableProperty] private string? _inlineUpdateStatus;
|
|
||||||
private bool _bannerDismissedThisSession;
|
private bool _bannerDismissedThisSession;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -206,21 +208,28 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
|
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
|
||||||
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
|
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
|
||||||
|
Func<UsageMonitorModalViewModel> usageMonitorVmFactory,
|
||||||
Func<MergeModalViewModel> mergeVmFactory,
|
Func<MergeModalViewModel> mergeVmFactory,
|
||||||
Func<RepoImportModalViewModel> repoImportVmFactory,
|
Func<RepoImportModalViewModel> repoImportVmFactory,
|
||||||
MissionControlViewModel missionControl)
|
MissionControlViewModel missionControl,
|
||||||
|
UsagePillViewModel usagePill)
|
||||||
{
|
{
|
||||||
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||||
MissionControl = missionControl;
|
MissionControl = missionControl;
|
||||||
|
UsagePill = usagePill;
|
||||||
|
UsagePill.OpenMonitorRequested += () => _ = OpenUsageMonitor();
|
||||||
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
||||||
MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
|
|
||||||
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
||||||
|
MissionControl.ErrorReported += FlashFooterError;
|
||||||
|
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
|
||||||
|
MissionControl.ConPtySessions.CollectionChanged += (_, _) => SyncInteractiveSessionChips();
|
||||||
_updateCheck = updateCheck;
|
_updateCheck = updateCheck;
|
||||||
_installerLocator = installerLocator;
|
_installerLocator = installerLocator;
|
||||||
_workerLocator = workerLocator;
|
_workerLocator = workerLocator;
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
|
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
|
||||||
_weeklyReportVmFactory = weeklyReportVmFactory;
|
_weeklyReportVmFactory = weeklyReportVmFactory;
|
||||||
|
_usageMonitorVmFactory = usageMonitorVmFactory;
|
||||||
_mergeVmFactory = mergeVmFactory;
|
_mergeVmFactory = mergeVmFactory;
|
||||||
_repoImportVmFactory = repoImportVmFactory;
|
_repoImportVmFactory = repoImportVmFactory;
|
||||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||||
@@ -228,12 +237,33 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
Tasks.NotesRequested += () => Details.ShowNotes();
|
Tasks.NotesRequested += () => Details.ShowNotes();
|
||||||
Tasks.PrepRequested += () => Details.ShowPrep();
|
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||||
Tasks.ErrorReported += FlashFooterError;
|
Tasks.ErrorReported += FlashFooterError;
|
||||||
|
Tasks.OpenConPtySessionRequested += taskId =>
|
||||||
|
{
|
||||||
|
OpenMissionControl();
|
||||||
|
_ = MissionControl.OpenConPtySessionAsync(taskId);
|
||||||
|
};
|
||||||
|
Tasks.OpenPlanningConPtyRequested += (taskId, resume) =>
|
||||||
|
{
|
||||||
|
OpenMissionControl();
|
||||||
|
_ = MissionControl.OpenPlanningConPtySessionAsync(taskId, resume);
|
||||||
|
};
|
||||||
|
Lists.LetClaudeHandleRequested += req =>
|
||||||
|
{
|
||||||
|
OpenMissionControl();
|
||||||
|
_ = MissionControl.OpenMergeHelperConPtySessionAsync(req.ListId, req.TaskIds);
|
||||||
|
};
|
||||||
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
||||||
Tasks.OpenListSettingsRequested += (_, _) =>
|
Tasks.OpenListSettingsRequested += (_, _) =>
|
||||||
{
|
{
|
||||||
if (Lists.SelectedList is { } row)
|
if (Lists.SelectedList is { } row)
|
||||||
Lists.OpenListSettingsCommand.Execute(row);
|
Lists.OpenListSettingsCommand.Execute(row);
|
||||||
};
|
};
|
||||||
|
Tasks.LetClaudeHandleRequested += (_, _) =>
|
||||||
|
{
|
||||||
|
if (Lists.SelectedList is { } row)
|
||||||
|
Lists.LetClaudeHandleListCommand.Execute(row);
|
||||||
|
};
|
||||||
|
Details.ErrorReported += FlashFooterError;
|
||||||
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
||||||
Details.DeleteFromList = row =>
|
Details.DeleteFromList = row =>
|
||||||
{
|
{
|
||||||
@@ -299,25 +329,15 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
if (_bannerDismissedThisSession) { IsUpdateBannerVisible = false; break; }
|
if (_bannerDismissedThisSession) { IsUpdateBannerVisible = false; break; }
|
||||||
UpdateBannerLatestVersion = _updateCheck.LatestVersion;
|
UpdateBannerLatestVersion = _updateCheck.LatestVersion;
|
||||||
IsUpdateBannerVisible = true;
|
IsUpdateBannerVisible = true;
|
||||||
InlineUpdateStatus = null;
|
|
||||||
break;
|
break;
|
||||||
case UpdateCheckStatus.UpToDate:
|
case UpdateCheckStatus.UpToDate:
|
||||||
IsUpdateBannerVisible = false;
|
IsUpdateBannerVisible = false;
|
||||||
ShowInlineStatus($"You're up to date (v{_updateCheck.CurrentVersion})");
|
|
||||||
break;
|
break;
|
||||||
case UpdateCheckStatus.CheckFailed:
|
case UpdateCheckStatus.CheckFailed:
|
||||||
ShowInlineStatus("Could not check for updates");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ShowInlineStatus(string text)
|
|
||||||
{
|
|
||||||
InlineUpdateStatus = text;
|
|
||||||
await Task.Delay(3000);
|
|
||||||
if (InlineUpdateStatus == text) InlineUpdateStatus = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenMissionControl()
|
private void OpenMissionControl()
|
||||||
{
|
{
|
||||||
@@ -325,6 +345,16 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
Dialogs.ShowMissionControl(MissionControl);
|
Dialogs.ShowMissionControl(MissionControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SyncInteractiveSessionChips()
|
||||||
|
{
|
||||||
|
if (MissionControl is null) return;
|
||||||
|
Tasks.SyncInteractiveSessions(
|
||||||
|
MissionControl.ConPtySessions
|
||||||
|
.Select(s => s.TaskId)
|
||||||
|
.Where(id => !string.IsNullOrEmpty(id))
|
||||||
|
.Select(id => id!));
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OpenAbout()
|
private async Task OpenAbout()
|
||||||
{
|
{
|
||||||
@@ -403,6 +433,23 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
finally { _weeklyReportOpen = false; }
|
finally { _weeklyReportOpen = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _usageMonitorOpen;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenUsageMonitor()
|
||||||
|
{
|
||||||
|
if (Dialogs is null || _usageMonitorOpen) return;
|
||||||
|
_usageMonitorOpen = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vm = _usageMonitorVmFactory();
|
||||||
|
vm.ErrorReported += FlashFooterError;
|
||||||
|
await vm.LoadAsync();
|
||||||
|
await Dialogs.ShowUsageMonitorAsync(vm);
|
||||||
|
}
|
||||||
|
finally { _usageMonitorOpen = false; }
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task CheckForUpdatesAsync()
|
private async Task CheckForUpdatesAsync()
|
||||||
{
|
{
|
||||||
@@ -473,7 +520,13 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
// WorkingDirectory must NOT stay empty: the child would inherit ours (<InstallDir>\app)
|
||||||
|
// and its locked current directory blocks the installer's own app\ rename.
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = System.IO.Path.GetTempPath(),
|
||||||
|
});
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
|
||||||
|
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, <see cref="TaskId"/>
|
||||||
|
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||||
|
{
|
||||||
|
public string? TaskId { get; }
|
||||||
|
|
||||||
|
// Only a task-based pane can be submitted for review (an ad-hoc directory session has no task).
|
||||||
|
public bool IsTaskBased => TaskId is not null;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _displayTitle;
|
||||||
|
|
||||||
|
public InteractiveTerminalViewModel Terminal { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Raised when the terminal failed to start — the host surfaces this via the footer error strip.</summary>
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
|
/// <summary>Set by the host (Mission Control) to remove this pane from its collection.</summary>
|
||||||
|
public Action<ConPtyPaneViewModel>? CloseRequested { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Raised when the user submits this task's hand-driven work for review; the host
|
||||||
|
/// commits the worktree and moves the task to WaitingForReview.</summary>
|
||||||
|
public event Action<string>? SubmitForReviewRequested;
|
||||||
|
|
||||||
|
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
||||||
|
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.
|
||||||
|
/// <paramref name="descriptorFactory"/> is resolved by the pane itself (in <see cref="Start"/>)
|
||||||
|
/// so the tile — and its starting spinner — is visible while the launch spec is still being
|
||||||
|
/// fetched. The host must wire its handlers and then call <see cref="Start"/>.</summary>
|
||||||
|
public ConPtyPaneViewModel(
|
||||||
|
string? taskId,
|
||||||
|
string displayTitle,
|
||||||
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> descriptorFactory)
|
||||||
|
{
|
||||||
|
TaskId = taskId;
|
||||||
|
_displayTitle = displayTitle;
|
||||||
|
_descriptorFactory = descriptorFactory;
|
||||||
|
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> _descriptorFactory;
|
||||||
|
|
||||||
|
/// <summary>Resolves the launch spec and spawns the session. Call after wiring
|
||||||
|
/// <see cref="ErrorReported"/> so a failed launch is not swallowed.</summary>
|
||||||
|
public void Start() => _ = StartAsync();
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task StartAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Terminal.Start(await _descriptorFactory());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Terminal.ReportStartFailure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ad-hoc pane — no task, no dedup.</summary>
|
||||||
|
public static ConPtyPaneViewModel CreateAdHoc(
|
||||||
|
string displayTitle,
|
||||||
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> descriptorFactory)
|
||||||
|
=> new(null, displayTitle, descriptorFactory);
|
||||||
|
|
||||||
|
private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(InteractiveTerminalViewModel.StartError) && Terminal.StartError is { Length: > 0 } error)
|
||||||
|
ErrorReported?.Invoke(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Close() => CloseRequested?.Invoke(this);
|
||||||
|
|
||||||
|
private bool CanSubmitForReview() => IsTaskBased;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSubmitForReview))]
|
||||||
|
private void SubmitForReview()
|
||||||
|
{
|
||||||
|
if (TaskId is { } id) SubmitForReviewRequested?.Invoke(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||||
|
Terminal.Kill();
|
||||||
|
Terminal.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Common contract for anything hosted as a pane in the Command Center — currently only
|
||||||
|
/// <see cref="ConPtyPaneViewModel"/>, kept as its own abstraction so the grid/tabs layout
|
||||||
|
/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMissionControlPane
|
||||||
|
{
|
||||||
|
string DisplayTitle { get; }
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
namespace ClaudeDo.Ui.ViewModels;
|
||||||
@@ -19,44 +20,52 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||||
private readonly Action<string> _onTaskUpdated;
|
private readonly Action<string> _onTaskUpdated;
|
||||||
private readonly Action _onConnectionRestored;
|
private readonly Action _onConnectionRestored;
|
||||||
private readonly Action<string> _onInteractiveStarted;
|
|
||||||
|
|
||||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
||||||
|
// review/merge/status machinery.
|
||||||
|
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
|
||||||
|
|
||||||
|
// Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs)
|
||||||
|
// binds one contract rather than a ConPTY-specific type.
|
||||||
|
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private int _columnCount = 1;
|
[ObservableProperty] private int _columnCount = 1;
|
||||||
|
|
||||||
private Action<string>? _openInApp;
|
[ObservableProperty]
|
||||||
public Action<string>? OpenInApp
|
[NotifyPropertyChangedFor(nameof(LayoutToggleLabel))]
|
||||||
{
|
private bool _isFocusMode;
|
||||||
get => _openInApp;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_openInApp = value;
|
|
||||||
foreach (var m in Monitors) m.OpenInAppRequested = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View-layer seam: show a detached monitor in its own window. Second arg is the re-dock callback
|
[ObservableProperty] private IMissionControlPane? _focusedPane;
|
||||||
// invoked when that window closes.
|
|
||||||
public Action<TaskMonitorViewModel, Action>? ShowDetached { get; set; }
|
public string LayoutToggleLabel => Loc.T(IsFocusMode ? "missionControl.overviewMode" : "missionControl.focusMode");
|
||||||
|
|
||||||
|
/// <summary>Surfaces a Command Center failure (e.g. a ConPTY launch spec fetch) — the shell
|
||||||
|
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
|
public Action<string>? OpenInApp { get; set; }
|
||||||
|
|
||||||
// View-layer seam: open the app Settings modal from the Mission Control window.
|
// View-layer seam: open the app Settings modal from the Mission Control window.
|
||||||
public Action? OpenSettingsRequested { get; set; }
|
public Action? OpenSettingsRequested { get; set; }
|
||||||
|
|
||||||
public bool HasMonitors => Monitors.Count > 0;
|
public bool HasPanes => Panes.Count > 0;
|
||||||
|
|
||||||
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
||||||
public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
|
public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
|
||||||
public bool HasQueued => Queued.Count > 0;
|
public bool HasQueued => Queued.Count > 0;
|
||||||
|
|
||||||
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
|
public UsagePillViewModel UsagePill { get; }
|
||||||
|
|
||||||
|
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, UsagePillViewModel usagePill)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
UsagePill = usagePill;
|
||||||
|
|
||||||
Monitors.CollectionChanged += OnMonitorsChanged;
|
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
||||||
|
Panes.CollectionChanged += OnPanesChanged;
|
||||||
|
|
||||||
_onTaskStarted = (slot, taskId, startedAt) => { EnsureMonitor(taskId); _ = RefreshQueueAsync(); };
|
_onTaskStarted = (slot, taskId, startedAt) => { _ = RefreshQueueAsync(); };
|
||||||
_worker.TaskStartedEvent += _onTaskStarted;
|
_worker.TaskStartedEvent += _onTaskStarted;
|
||||||
|
|
||||||
_onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
|
_onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
|
||||||
@@ -65,18 +74,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_onTaskUpdated = taskId => _ = RefreshQueueAsync();
|
_onTaskUpdated = taskId => _ = RefreshQueueAsync();
|
||||||
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
||||||
|
|
||||||
_onConnectionRestored = () => { SeedActive(); _ = RefreshQueueAsync(); };
|
_onConnectionRestored = () => { _ = RefreshQueueAsync(); };
|
||||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||||
|
|
||||||
_onInteractiveStarted = taskId =>
|
|
||||||
{
|
|
||||||
EnsureMonitor(taskId);
|
|
||||||
var m = Monitors.FirstOrDefault(x => x.SubscribedTaskId == taskId);
|
|
||||||
m?.SetInteractiveLive(true);
|
|
||||||
};
|
|
||||||
_worker.InteractiveSessionStartedEvent += _onInteractiveStarted;
|
|
||||||
|
|
||||||
SeedActive();
|
|
||||||
_ = RefreshQueueAsync();
|
_ = RefreshQueueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,19 +86,26 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var rows = await ctx.Tasks.AsNoTracking()
|
var rows = await ctx.Tasks.AsNoTracking()
|
||||||
.Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued)
|
.Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued
|
||||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
|| t.Status == ClaudeDo.Data.Models.TaskStatus.Running)
|
||||||
.Select(t => new { t.Id, t.Title, t.BlockedByTaskId })
|
.OrderBy(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Running ? 0 : 1)
|
||||||
|
.ThenBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.Select(t => new { t.Id, t.Title, t.BlockedByTaskId, t.Status })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
Queued.Clear();
|
Queued.Clear();
|
||||||
foreach (var r in rows)
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
var id = r.Id;
|
||||||
Queued.Add(new QueuedTaskViewModel
|
Queued.Add(new QueuedTaskViewModel
|
||||||
{
|
{
|
||||||
Id = r.Id,
|
Id = id,
|
||||||
Title = r.Title ?? string.Empty,
|
Title = r.Title ?? string.Empty,
|
||||||
IsBlocked = r.BlockedByTaskId != null,
|
IsBlocked = r.BlockedByTaskId != null,
|
||||||
|
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
|
||||||
|
OpenInAppCommand = new RelayCommand(() => OpenInApp?.Invoke(id)),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
OnPropertyChanged(nameof(HasQueued));
|
OnPropertyChanged(nameof(HasQueued));
|
||||||
}
|
}
|
||||||
catch { /* best-effort queue refresh */ }
|
catch { /* best-effort queue refresh */ }
|
||||||
@@ -124,92 +131,194 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
await RefreshQueueAsync();
|
await RefreshQueueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SeedActive()
|
[RelayCommand]
|
||||||
{
|
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
||||||
foreach (var a in _worker.GetActiveTasks())
|
|
||||||
EnsureMonitor(a.TaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureMonitor(string taskId)
|
[RelayCommand]
|
||||||
|
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
|
||||||
|
|
||||||
|
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
||||||
|
// Command Center pane (task-based only).
|
||||||
|
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(taskId)) return;
|
if (string.IsNullOrEmpty(taskId)) return;
|
||||||
if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;
|
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||||
|
{
|
||||||
var monitor = new TaskMonitorViewModel(_dbFactory, _worker);
|
FocusedPane = existing;
|
||||||
monitor.SetTaskId(taskId);
|
return;
|
||||||
monitor.OpenInAppRequested = _openInApp;
|
|
||||||
monitor.DetachRequested = Detach;
|
|
||||||
Monitors.Add(monitor);
|
|
||||||
_ = HydrateAsync(monitor, taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Detach(TaskMonitorViewModel monitor)
|
var title = taskId;
|
||||||
{
|
|
||||||
if (!Monitors.Contains(monitor)) return;
|
|
||||||
monitor.IsDetached = true;
|
|
||||||
Monitors.Remove(monitor); // drop from grid — do NOT dispose; it keeps streaming
|
|
||||||
ShowDetached?.Invoke(monitor, () => ReDock(monitor));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReDock(TaskMonitorViewModel monitor)
|
|
||||||
{
|
|
||||||
monitor.IsDetached = false;
|
|
||||||
if (!Monitors.Contains(monitor) && monitor.SubscribedTaskId is not null)
|
|
||||||
Monitors.Add(monitor); // back into the grid
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task HydrateAsync(TaskMonitorViewModel monitor, string taskId)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
if (entity is null || monitor.SubscribedTaskId != taskId) return;
|
if (entity?.Title is { Length: > 0 } t) title = t;
|
||||||
monitor.ApplyState(entity.Status);
|
|
||||||
monitor.Title = entity.Title;
|
|
||||||
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
|
||||||
monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
|
||||||
await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);
|
|
||||||
|
|
||||||
// Re-attach: if the task is blocked on an AskUser question right now, surface it.
|
|
||||||
var pending = await _worker.GetPendingQuestionAsync(taskId);
|
|
||||||
if (pending is not null && monitor.SubscribedTaskId == taskId)
|
|
||||||
monitor.SetPendingQuestion(pending.QuestionId, pending.Question);
|
|
||||||
}
|
}
|
||||||
catch { /* best-effort hydrate */ }
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
|
() => DescribeAsync(() => _worker.GetInteractiveLaunchSpecAsync(taskId))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
// Starts (or resumes) a planning session and hosts it as an embedded ConPTY Command Center
|
||||||
private void ClearFinished()
|
// pane — the ConPTY replacement for the old external wt planning window. Deduped by TaskId
|
||||||
|
// like OpenConPtySessionAsync.
|
||||||
|
public async System.Threading.Tasks.Task OpenPlanningConPtySessionAsync(string taskId, bool resume)
|
||||||
{
|
{
|
||||||
foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled || m.IsWaitingForReview).ToList())
|
if (string.IsNullOrEmpty(taskId)) return;
|
||||||
|
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||||
{
|
{
|
||||||
Monitors.Remove(m);
|
FocusedPane = existing;
|
||||||
m.Dispose();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = taskId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
if (entity?.Title is { Length: > 0 } t) title = t;
|
||||||
|
}
|
||||||
|
catch { /* best-effort title lookup */ }
|
||||||
|
title += Loc.T("missionControl.planningTitleSuffix");
|
||||||
|
|
||||||
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title, () => DescribeAsync(() => resume
|
||||||
|
? _worker.GetPlanningResumeLaunchSpecAsync(taskId)
|
||||||
|
: _worker.GetPlanningStartLaunchSpecAsync(taskId))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ad-hoc (task-less) ConPTY session in a user-chosen directory. Never deduped — every call
|
||||||
|
// opens a fresh pane, unlike the task-based OpenConPtySessionAsync above.
|
||||||
|
public System.Threading.Tasks.Task OpenAdHocConPtySessionAsync(string directory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(directory)) return System.Threading.Tasks.Task.CompletedTask;
|
||||||
|
|
||||||
|
var title = Path.GetFileName(directory.TrimEnd('\\', '/'));
|
||||||
|
if (string.IsNullOrEmpty(title)) title = directory;
|
||||||
|
|
||||||
|
AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title,
|
||||||
|
() => DescribeAsync(() => _worker.GetAdHocLaunchSpecAsync(directory))));
|
||||||
|
return System.Threading.Tasks.Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List-handler session over a hand-picked set of tasks ("Let Claude handle it"). Task-based:
|
||||||
|
// creates one new ClaudeDo task per run to own the session (title/diff/result), deduped by
|
||||||
|
// TaskId like OpenConPtySessionAsync. The handler still merges the tasks it handles itself
|
||||||
|
// (no worktree of its own) — see TaskEntity.HandlerBaseCommit/HandlerHeadCommit.
|
||||||
|
public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
||||||
|
{
|
||||||
|
if (taskIds is not { Count: > 0 }) return;
|
||||||
|
|
||||||
|
var title = Loc.T("missionControl.mergeHelperTitle");
|
||||||
|
var listName = listId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId);
|
||||||
|
if (list?.Name is { Length: > 0 } name) { listName = name; title = $"{title} — {name}"; }
|
||||||
|
}
|
||||||
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
|
string taskId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
taskId = await _worker.CreateMergeHelperTaskAsync(taskIds, listId,
|
||||||
|
Loc.T("missionControl.mergeHelperTaskTitle", listName),
|
||||||
|
Loc.T("missionControl.mergeHelperTaskDescriptionHeader"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||||
|
{
|
||||||
|
FocusedPane = existing;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
|
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wires a freshly built pane and shows it immediately — the pane resolves its own launch spec,
|
||||||
|
// so the tile is on screen (spinner running) while the worker is still preparing the worktree.
|
||||||
|
private void AddConPtyPane(ConPtyPaneViewModel pane)
|
||||||
|
{
|
||||||
|
pane.ErrorReported += OnConPtyPaneError;
|
||||||
|
pane.CloseRequested += CloseConPtySession;
|
||||||
|
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||||
|
ConPtySessions.Add(pane);
|
||||||
|
pane.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async System.Threading.Tasks.Task<TerminalLaunchDescriptor> DescribeAsync(
|
||||||
|
Func<System.Threading.Tasks.Task<LaunchSpec>> fetch)
|
||||||
|
{
|
||||||
|
var spec = await fetch();
|
||||||
|
return new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConPtyPaneError(string message)
|
||||||
|
=> ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", message));
|
||||||
|
|
||||||
|
// Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive
|
||||||
|
// session is finished). The worker commits the worktree and moves the task to WaitingForReview.
|
||||||
|
private async void OnPaneSubmitForReview(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _worker.SubmitTaskForReviewAsync(taskId);
|
||||||
|
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } pane)
|
||||||
|
CloseConPtySession(pane);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("missionControl.submitForReviewFailed", ex.Message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
private void CloseConPtySession(ConPtyPaneViewModel pane)
|
||||||
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
|
||||||
|
|
||||||
public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
|
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(dragged, target)) return;
|
if (!ConPtySessions.Contains(pane)) return;
|
||||||
var from = Monitors.IndexOf(dragged);
|
pane.ErrorReported -= OnConPtyPaneError;
|
||||||
var to = Monitors.IndexOf(target);
|
pane.CloseRequested -= CloseConPtySession;
|
||||||
if (from < 0 || to < 0) return;
|
ConPtySessions.Remove(pane);
|
||||||
Monitors.Move(from, to);
|
pane.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
// Mirrors ConPtySessions' add/remove 1:1 into Panes (typed as the pane abstraction).
|
||||||
|
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
ColumnCount = Monitors.Count switch
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
Panes.Insert(e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
Panes.RemoveAt(e.OldStartingIndex);
|
||||||
|
break;
|
||||||
|
default: // Reset
|
||||||
|
Panes.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
ColumnCount = Panes.Count switch
|
||||||
{
|
{
|
||||||
<= 1 => 1,
|
<= 1 => 1,
|
||||||
<= 4 => 2,
|
<= 4 => 2,
|
||||||
_ => 3,
|
_ => 3,
|
||||||
};
|
};
|
||||||
OnPropertyChanged(nameof(HasMonitors));
|
OnPropertyChanged(nameof(HasPanes));
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IMissionControlPane added)
|
||||||
|
FocusedPane = added;
|
||||||
|
else if (e.Action == NotifyCollectionChangedAction.Remove && ReferenceEquals(FocusedPane, e.OldItems?[0]))
|
||||||
|
FocusedPane = Panes.LastOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -218,17 +327,25 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.TaskFinishedEvent -= _onTaskFinished;
|
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||||
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
|
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
Panes.CollectionChanged -= OnPanesChanged;
|
||||||
foreach (var m in Monitors) m.Dispose();
|
foreach (var c in ConPtySessions.ToList())
|
||||||
Monitors.Clear();
|
{
|
||||||
|
c.ErrorReported -= OnConPtyPaneError;
|
||||||
|
c.CloseRequested -= CloseConPtySession;
|
||||||
|
c.Dispose();
|
||||||
|
}
|
||||||
|
ConPtySessions.Clear();
|
||||||
|
Panes.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Read-only display row for a queued task in the Mission Control side strip.</summary>
|
/// <summary>Read-only display row for a queued or running task in the Mission Control side strip.</summary>
|
||||||
public sealed class QueuedTaskViewModel
|
public sealed class QueuedTaskViewModel
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string Title { get; init; }
|
public required string Title { get; init; }
|
||||||
public bool IsBlocked { get; init; }
|
public bool IsBlocked { get; init; }
|
||||||
|
public bool IsRunning { get; init; }
|
||||||
|
public IRelayCommand? OpenInAppCommand { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ public sealed class DiffFileViewModel
|
|||||||
|
|
||||||
/// A text file that produced no diff hunks (e.g. a newly added empty file).
|
/// A text file that produced no diff hunks (e.g. a newly added empty file).
|
||||||
public bool IsEmptyContent => !IsBinary && Lines.Count == 0;
|
public bool IsEmptyContent => !IsBinary && Lines.Count == 0;
|
||||||
|
|
||||||
|
/// A rename with no content change — showing "+0 −0" for it is misleading.
|
||||||
|
public bool IsPureRename => Status == DiffFileStatus.Renamed && Additions == 0 && Deletions == 0;
|
||||||
|
|
||||||
|
/// Header path label: for a rename, spell out old → new; otherwise just the path.
|
||||||
|
public string HeaderPath => Status == DiffFileStatus.Renamed && !string.IsNullOrEmpty(OldPath)
|
||||||
|
? $"{OldPath} → {Path}"
|
||||||
|
: Path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row in the planning subtask list (left pane in Planning mode).
|
/// One row in the planning subtask list (left pane in Planning mode).
|
||||||
@@ -72,7 +80,7 @@ public sealed partial class DiffTreeNodeViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isExpanded = true;
|
[ObservableProperty] private bool _isExpanded = true;
|
||||||
|
|
||||||
public string? StatusCode => File?.StatusCode;
|
public string? StatusCode => File?.StatusCode;
|
||||||
public bool ShowStats => File is { IsBinary: false };
|
public bool ShowStats => File is { IsBinary: false, IsPureRename: false };
|
||||||
public int Additions => File?.Additions ?? 0;
|
public int Additions => File?.Additions ?? 0;
|
||||||
public int Deletions => File?.Deletions ?? 0;
|
public int Deletions => File?.Deletions ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|||||||
DisplayedDiff = "";
|
DisplayedDiff = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DisplayedDiff = "";
|
||||||
|
CombinedWarning = Loc.T("vm.planningDiff.buildFailed", ex.Message);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsLoadingCombined = false;
|
IsLoadingCombined = false;
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _name = "";
|
[ObservableProperty] private string _name = "";
|
||||||
[ObservableProperty] private string _workingDir = "";
|
[ObservableProperty] private string _workingDir = "";
|
||||||
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
||||||
|
// A manual list holds reminders: tasks created here start out manual (TaskEntity.IsManual).
|
||||||
|
[ObservableProperty] private bool _isManual;
|
||||||
|
// Optional post-merge verification command (build/test), run in WorkingDir after a merge
|
||||||
|
// lands; a non-zero exit keeps the task out of Done instead of silently reporting merged.
|
||||||
|
[ObservableProperty] private string _verifyCommand = "";
|
||||||
|
|
||||||
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
||||||
|
|
||||||
@@ -49,14 +54,18 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
string name,
|
string name,
|
||||||
string? workingDir,
|
string? workingDir,
|
||||||
string defaultCommitType,
|
string defaultCommitType,
|
||||||
|
bool isManual = false,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ListId = listId;
|
ListId = listId;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
IsManual = isManual;
|
||||||
WorkingDir = workingDir ?? "";
|
WorkingDir = workingDir ?? "";
|
||||||
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
|
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
|
||||||
|
|
||||||
await Agent.LoadForListAsync(listId, ct);
|
await Agent.LoadForListAsync(listId, ct);
|
||||||
|
var cfg = await _worker.GetListConfigAsync(listId);
|
||||||
|
VerifyCommand = cfg?.VerifyCommand ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -66,9 +75,10 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
ListId,
|
ListId,
|
||||||
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
|
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
|
||||||
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
|
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
|
||||||
DefaultCommitType));
|
DefaultCommitType,
|
||||||
|
IsManual));
|
||||||
|
|
||||||
await Agent.SaveAsync();
|
await Agent.SaveAsync(string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand);
|
||||||
|
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
|||||||
IEnumerable<WorkerLogEntry> items = WarnErrorOnly
|
IEnumerable<WorkerLogEntry> items = WarnErrorOnly
|
||||||
? _all.Where(e => e.Level is WorkerLogLevel.Warn or WorkerLogLevel.Error)
|
? _all.Where(e => e.Level is WorkerLogLevel.Warn or WorkerLogLevel.Error)
|
||||||
: _all;
|
: _all;
|
||||||
foreach (var e in items)
|
foreach (var e in items.Reverse())
|
||||||
Rows.Add(new LogVisualizerRow(e.TimestampUtc.ToLocalTime().ToString("HH:mm:ss"), e.Message, e.Level));
|
Rows.Add(new LogVisualizerRow(e.TimestampUtc.ToLocalTime().ToString("HH:mm:ss"), e.Message, e.Level));
|
||||||
StatusText = Rows.Count == 0
|
StatusText = Rows.Count == 0
|
||||||
? Loc.T("modals.logVisualizer.empty")
|
? Loc.T("modals.logVisualizer.empty")
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public required string StatusText { get; init; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Let Claude handle it" task picker: lists the non-terminal tasks of a list (or all lists),
|
||||||
|
/// pre-ticks the actionable ones, and resolves <see cref="Result"/> with the ordered selected
|
||||||
|
/// task ids (null on cancel).
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private string _listId = "";
|
||||||
|
|
||||||
|
public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private string _scopeLabel = "";
|
||||||
|
|
||||||
|
public bool HasTasks => Tasks.Count > 0;
|
||||||
|
public bool CanConfirm => Tasks.Any(t => t.IsSelected);
|
||||||
|
|
||||||
|
public TaskCompletionSource<IReadOnlyList<string>?> Result { get; } = new();
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
public MergeHelperSelectionModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||||
|
=> _dbFactory = dbFactory;
|
||||||
|
|
||||||
|
public void Configure(string listId, string listName)
|
||||||
|
{
|
||||||
|
_listId = listId;
|
||||||
|
ScopeLabel = Loc.T("modals.mergeHelper.scopeList", listName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
foreach (var row in Tasks) row.PropertyChanged -= OnRowChanged;
|
||||||
|
Tasks.Clear();
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var candidates = await ctx.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled)
|
||||||
|
.Where(t => t.ListId == _listId)
|
||||||
|
// Manual tasks are reminders the user owns — never offer them to the handler.
|
||||||
|
.Where(t => !t.IsManual)
|
||||||
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.Select(t => new { t.Id, t.Title, t.Status })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
foreach (var c in candidates)
|
||||||
|
{
|
||||||
|
var row = new MergeHelperTaskRowViewModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
Title = c.Title,
|
||||||
|
StatusText = c.Status.ToString(),
|
||||||
|
IsSelected = IsTickedByDefault(c.Status),
|
||||||
|
};
|
||||||
|
row.PropertyChanged += OnRowChanged;
|
||||||
|
Tasks.Add(row);
|
||||||
|
}
|
||||||
|
OnPropertyChanged(nameof(HasTasks));
|
||||||
|
OnPropertyChanged(nameof(CanConfirm));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actionable statuses are pre-ticked; Running/WaitingForChildren are listed but unticked
|
||||||
|
// (the helper would only poll them). Done/Cancelled never make it into the list.
|
||||||
|
internal static bool IsTickedByDefault(TaskStatus status) => status
|
||||||
|
is TaskStatus.Idle or TaskStatus.Queued or TaskStatus.WaitingForReview or TaskStatus.Failed;
|
||||||
|
|
||||||
|
private void OnRowChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(MergeHelperTaskRowViewModel.IsSelected))
|
||||||
|
OnPropertyChanged(nameof(CanConfirm));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SelectAll()
|
||||||
|
{
|
||||||
|
foreach (var t in Tasks) t.IsSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SelectNone()
|
||||||
|
{
|
||||||
|
foreach (var t in Tasks) t.IsSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Confirm()
|
||||||
|
{
|
||||||
|
Result.TrySetResult(Tasks.Where(t => t.IsSelected).Select(t => t.Id).ToList());
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
Result.TrySetResult(null);
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Localization;
|
using ClaudeDo.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Agent;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
|
|
||||||
@@ -14,6 +18,9 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
[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;
|
[ObservableProperty] private int _maxParallelExecutions = 1;
|
||||||
|
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||||
|
[ObservableProperty] private int _usageGateFiveHourPct = 80;
|
||||||
|
[ObservableProperty] private int _usageGateSevenDayPct = 90;
|
||||||
// Newline-separated path prefixes excluded from the weekly report.
|
// Newline-separated path prefixes excluded from the weekly report.
|
||||||
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
||||||
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
||||||
@@ -22,6 +29,46 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
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;
|
||||||
|
|
||||||
|
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
|
||||||
|
|
||||||
|
public ObservableCollection<AccentPresetSwatchViewModel> AccentPresetSwatches { get; } = new();
|
||||||
|
private Action<string>? _persistAccent;
|
||||||
|
|
||||||
|
public void InitAccentPresets(string saved, Action<string> persist)
|
||||||
|
{
|
||||||
|
_persistAccent = persist;
|
||||||
|
var current = AccentPresets.Find(saved);
|
||||||
|
AccentPresetSwatches.Clear();
|
||||||
|
foreach (var p in AccentPresets.All)
|
||||||
|
AccentPresetSwatches.Add(new AccentPresetSwatchViewModel(p, p.Name == current.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SelectAccentPreset(AccentPreset preset)
|
||||||
|
{
|
||||||
|
foreach (var s in AccentPresetSwatches)
|
||||||
|
s.IsSelected = s.Preset.Name == preset.Name;
|
||||||
|
AccentPresetService.Apply(preset);
|
||||||
|
_persistAccent?.Invoke(preset.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One editable row per model alias: the effort and turn budget a run gets under that
|
||||||
|
/// model. Supplies the global defaults; list- and task-level max-turns overrides still win.</summary>
|
||||||
|
public ObservableCollection<ModelPresetRowViewModel> ModelPresets { get; } = new();
|
||||||
|
|
||||||
|
public void LoadModelPresets(IReadOnlyCollection<ModelPresetDto>? presets)
|
||||||
|
{
|
||||||
|
ModelPresets.Clear();
|
||||||
|
var source = presets is { Count: > 0 }
|
||||||
|
? presets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)).ToList()
|
||||||
|
: Data.Models.ModelPresets.Defaults.ToList();
|
||||||
|
foreach (var p in Data.Models.ModelPresets.Parse(Data.Models.ModelPresets.Serialize(source)))
|
||||||
|
ModelPresets.Add(new ModelPresetRowViewModel(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ModelPresetDto> ModelPresetDtos()
|
||||||
|
=> ModelPresets.Select(r => new ModelPresetDto(r.Model, r.Effort, (int)r.MaxTurns)).ToList();
|
||||||
|
|
||||||
public GeneralSettingsTabViewModel() { }
|
public GeneralSettingsTabViewModel() { }
|
||||||
|
|
||||||
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
|
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
|
||||||
@@ -45,10 +92,49 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string? Validate()
|
public string? Validate()
|
||||||
{
|
{
|
||||||
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
|
||||||
return "Max turns must be between 1 and 200.";
|
|
||||||
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||||
return "Max parallel executions must be between 1 and 20.";
|
return "Max parallel executions must be between 1 and 20.";
|
||||||
|
if (UsageGateFiveHourPct < 0 || UsageGateFiveHourPct > 100)
|
||||||
|
return "Usage gate (5h) must be between 0 and 100.";
|
||||||
|
if (UsageGateSevenDayPct < 0 || UsageGateSevenDayPct > 100)
|
||||||
|
return "Usage gate (7d) must be between 0 and 100.";
|
||||||
|
foreach (var row in ModelPresets)
|
||||||
|
if (row.MaxTurns is < 1 or > 200)
|
||||||
|
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Loads the installed-skills checkbox list, reflecting the given selection.</summary>
|
||||||
|
public async Task LoadSessionSkillsAsync(IWorkerClient worker, IReadOnlyCollection<string>? selected)
|
||||||
|
{
|
||||||
|
var installed = await worker.GetSessionSkillsAsync();
|
||||||
|
var selectedSet = selected is null ? new HashSet<string>() : new HashSet<string>(selected);
|
||||||
|
SessionSkills.Clear();
|
||||||
|
foreach (var s in installed)
|
||||||
|
SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string>? SelectedSessionSkillNames()
|
||||||
|
{
|
||||||
|
var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
||||||
|
return names.Count == 0 ? null : names;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
||||||
|
public sealed partial class ModelPresetRowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Model { get; }
|
||||||
|
public IReadOnlyList<string> EffortLevels { get; } = EffortRegistry.Levels;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _effort;
|
||||||
|
// decimal so it binds straight to a NumericUpDown, like the other numeric settings.
|
||||||
|
[ObservableProperty] private decimal _maxTurns;
|
||||||
|
|
||||||
|
public ModelPresetRowViewModel(ModelPreset preset)
|
||||||
|
{
|
||||||
|
Model = preset.Model;
|
||||||
|
_effort = preset.Effort;
|
||||||
|
_maxTurns = preset.MaxTurns;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user