Compare commits
255
Commits
f8f20bf6ed
...
v2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
711374e858 | ||
|
|
faf6104645 | ||
|
|
3eea2b7c96 | ||
|
|
afe7218b7c | ||
|
|
fd1e38fb7f | ||
|
|
e7fa373a74 | ||
|
|
7c9ff18ced | ||
|
|
84034e8395 | ||
|
|
8e1732a3a0 | ||
|
|
786eb2877f | ||
|
|
bdda98eccd | ||
|
|
9c292e5080 | ||
|
|
1fe72a1fe2 | ||
|
|
140b8e1551 | ||
|
|
9effddeb2c | ||
|
|
30e87e698e | ||
|
|
d8a043fae7 | ||
|
|
10342bc562 | ||
|
|
917301d61c | ||
|
|
c7f8280106 | ||
|
|
bec26b2232 | ||
|
|
05aec8ebfa | ||
|
|
946d26cc4b | ||
|
|
3b629c218f | ||
|
|
9eb54a0d2f | ||
|
|
1c94fbdb14 | ||
|
|
7f4dc8b973 | ||
|
|
f6ecfc995f | ||
|
|
f63be285a2 | ||
|
|
e2fad88f37 | ||
|
|
fbcffce79c | ||
|
|
5f6e7480f2 | ||
|
|
4e2798b400 | ||
|
|
b1bd91292f | ||
|
|
283310a3fd | ||
|
|
15a3e65508 | ||
|
|
5a21d673c1 | ||
|
|
42da840066 | ||
|
|
aa7a49f634 | ||
|
|
7b6a8f0852 | ||
|
|
d00899b655 | ||
|
|
66907d24c9 | ||
|
|
38defee3d8 | ||
|
|
d80a57836c | ||
|
|
178fd25b55 | ||
|
|
df84fc3f2c | ||
|
|
ea16da2756 | ||
|
|
f86b78593e | ||
|
|
19340fd9de | ||
|
|
0a119f1450 | ||
|
|
167d2fec6a | ||
|
|
4022bd7197 | ||
|
|
c4f74a7aea | ||
|
|
08a4f97a78 | ||
|
|
eb0ddb56d3 | ||
|
|
60eb671e8f | ||
|
|
134b9fb598 | ||
|
|
9301bbc81a | ||
|
|
637886f33a | ||
|
|
3cb4802f38 | ||
|
|
8716dd8e3a | ||
|
|
d8ff8cc110 | ||
|
|
f7e946e472 | ||
|
|
6a0c0f59a5 | ||
|
|
5be4b5c5fb | ||
|
|
3f9f047955 | ||
|
|
5231ad6b86 | ||
|
|
d598a539bc | ||
|
|
1fb2e34f85 | ||
|
|
b3e099ca01 | ||
|
|
0993eb0e75 | ||
|
|
bae8921201 | ||
|
|
23a93ce0bb | ||
|
|
29a294b7f3 | ||
|
|
ca4377e641 | ||
|
|
d5eec75bea | ||
|
|
18479c023e | ||
|
|
869dd25a23 | ||
|
|
c4d1acc75b | ||
|
|
378a92c156 | ||
|
|
983c177c9a | ||
|
|
3e4e4a03f7 | ||
|
|
92767c646e | ||
|
|
e779e13654 | ||
|
|
4847c5c0a4 | ||
|
|
43fb506e87 | ||
|
|
b75a7b1b5a | ||
|
|
824f785fd0 | ||
|
|
0d1475cb7a | ||
|
|
cfe23cdd23 | ||
|
|
cee051bb6d | ||
|
|
23c3065f20 | ||
|
|
80a2de6c74 | ||
|
|
17c7ff517a | ||
|
|
8b347de131 | ||
|
|
619bc0c38d | ||
|
|
96da9fbae5 | ||
|
|
1ac9ced0bd | ||
|
|
8cbe1adb32 | ||
|
|
23ff3916cc | ||
|
|
360ff77e18 | ||
|
|
e272053e72 | ||
|
|
74ca2e0dcd | ||
|
|
0cba9f9640 | ||
|
|
c6534165b2 | ||
|
|
290b4a602a | ||
|
|
fe73f45b74 | ||
|
|
d2a08d2cda | ||
|
|
8194dadb6a | ||
|
|
fb1d799b82 | ||
|
|
12fdb55a8e | ||
|
|
eee5c99e2f | ||
|
|
37df51475e | ||
|
|
53b666dfbd | ||
|
|
cd5501e6a6 | ||
|
|
b5417f6b09 | ||
|
|
7e739afafb | ||
|
|
e9e4ad8fbc | ||
|
|
d4af345ac3 | ||
|
|
ddeded988a | ||
|
|
c27a179d2b | ||
|
|
1448794748 | ||
|
|
51ef488d2f | ||
|
|
49046310ef |
@@ -145,18 +145,19 @@ jobs:
|
||||
ZIP_NAME="ClaudeDo-${VERSION}-win-x64.zip"
|
||||
( cd bundle && zip -r -q "../assets/${ZIP_NAME}" app worker )
|
||||
|
||||
# 2) Installer single-file exe (renamed)
|
||||
# 2) Installer single-file exe — STABLE name (no version) so the download URL
|
||||
# (…/releases/latest/download/ClaudeDo.Installer.exe) stays permanent.
|
||||
INSTALLER_EXE=$(ls out/installer/*.exe | head -n 1)
|
||||
if [ -z "$INSTALLER_EXE" ]; then
|
||||
echo "::error::No .exe produced by installer publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$INSTALLER_EXE" "assets/ClaudeDo.Installer-${VERSION}.exe"
|
||||
cp "$INSTALLER_EXE" "assets/ClaudeDo.Installer.exe"
|
||||
|
||||
# 3) Checksums (sha256, relative filenames)
|
||||
( cd assets && sha256sum \
|
||||
"ClaudeDo-${VERSION}-win-x64.zip" \
|
||||
"ClaudeDo.Installer-${VERSION}.exe" \
|
||||
"ClaudeDo.Installer.exe" \
|
||||
> checksums.txt )
|
||||
|
||||
echo "--- assets ---"
|
||||
@@ -200,7 +201,7 @@ jobs:
|
||||
cd "$WORK/src/assets"
|
||||
for f in \
|
||||
"ClaudeDo-${VERSION}-win-x64.zip" \
|
||||
"ClaudeDo.Installer-${VERSION}.exe" \
|
||||
"ClaudeDo.Installer.exe" \
|
||||
"checksums.txt"
|
||||
do
|
||||
echo "Uploading: $f"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Local dev worktrees (created by using-git-worktrees skill)
|
||||
.worktrees/
|
||||
.claude/worktrees/
|
||||
|
||||
# Brainstorming visual companion artifacts
|
||||
.superpowers/
|
||||
|
||||
+272
@@ -1,5 +1,277 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
### Features
|
||||
|
||||
- diff Merge opens the 3-pane editor + conflict overview ruler (29a294b)
|
||||
- toggle add/remove per side, MAIN/INCOMING labels, files readout (ca4377e)
|
||||
- additive conflict accept — stack ours/theirs in click order (d5eec75)
|
||||
- add accept-both control to the 3-pane conflict gutter (18479c0)
|
||||
- Rider-style 3-pane conflict editor view (c4d1acc)
|
||||
- unify planning conflicts onto the resolver + 3-pane VM foundation (378a92c)
|
||||
- move review feedback to the Output tab + review/worktree polish (3e4e4a0)
|
||||
- in-app 3-way merge editor (chunk 2b) (92767c6)
|
||||
- real conflict-hunk parsing pipeline (chunk 2 backend) (e779e13)
|
||||
- My Day actions, orphan-aware grouping, menu restructure (4847c5c)
|
||||
- unify review actions into the Git-tab cockpit (43fb506)
|
||||
- carry ownerId on sync to prepare for multi-user (cee051b)
|
||||
- gate access on Zitadel "user" project role (23c3065)
|
||||
- Online Inbox settings tab + auth-code/PKCE login (80a2de6)
|
||||
- Online Inbox config + auth hub plumbing (Phase 2) (17c7ff5)
|
||||
- real ZitadelAuthProvider (refresh-token grant, auth-code+PKCE) (619bc0c)
|
||||
- Online Inbox sync engine (Phase 1) (1ac9ced)
|
||||
- let Claude set the cheapest model per generated task via MCP (c27a179)
|
||||
|
||||
### Fixes
|
||||
|
||||
- unresolved conflicts compose to empty, not Ours (+ review nits) (23a93ce)
|
||||
- harden 3-pane editor + document the new conflict resolver (869dd25)
|
||||
- invalidate cached access token when the signed-in user changes (cfe23cd)
|
||||
- preserve API base path in Online Inbox client (8b347de)
|
||||
- queue dispatches skip the StartRunning re-claim (74ca2e0)
|
||||
- document and test Queued→Failed guard in FailAsync (fe73f45)
|
||||
- stateless AbortPlanningMerge after worker restart mid-merge (fb1d799)
|
||||
- route FinalizeParentDoneAsync through TaskStateService (e9e4ad8)
|
||||
|
||||
### Refactoring
|
||||
|
||||
- bring IWorkerClient to parity with WorkerClient (b5417f6)
|
||||
|
||||
### Documentation
|
||||
|
||||
- spec + plan for Rider-style 3-pane merge editor (983c177)
|
||||
- KunsZitadel is server-side only; desktop uses an OIDC client flow (96da9fb)
|
||||
- API contract, desktop design spec, and implementation plan (8cbe1ad)
|
||||
- close out the review round in open.md, sync CLAUDE.md with merges (23ff391)
|
||||
- record correctness-review findings (4 confirmed as tasks) (ddeded9)
|
||||
- record review findings as refactoring backlog (1448794)
|
||||
- spec + plan for per-task model override via MCP (51ef488)
|
||||
- refresh CLAUDE.md files and open.md to current code state (4904631)
|
||||
- update for v1.8.0 (f8f20bf)
|
||||
|
||||
## v1.8.0 — 2026-06-09
|
||||
|
||||
### Features
|
||||
|
||||
@@ -10,7 +10,11 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
||||
- **ClaudeDo.Ui** — Views, ViewModels, SignalR client (MVVM with CommunityToolkit.Mvvm)
|
||||
- **ClaudeDo.Data** — SQLite data layer, repositories, models, GitService
|
||||
- **ClaudeDo.Worker** — ASP.NET Core hosted service, task queue, Claude CLI runner
|
||||
- **ClaudeDo.Worker.Tests** — xUnit integration tests with real SQLite and real git
|
||||
- **ClaudeDo.Localization** — `locales/en.json` + `locales/de.json` and the lookup service
|
||||
- **ClaudeDo.Installer** — WPF (`UseWPF`) setup app; install/update/uninstall step pipeline
|
||||
- **tests/** — six xUnit projects (Worker, Data, Ui, Localization, Installer, Releases); Worker.Tests run real SQLite and real git
|
||||
|
||||
Each project has its own `CLAUDE.md` — those are the living per-project docs.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -35,7 +39,7 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
||||
- EF Core migrations manage schema (Migrations/ folder in ClaudeDo.Data)
|
||||
- `IDbContextFactory<ClaudeDoDbContext>` used by singleton consumers (e.g. Worker)
|
||||
- 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
|
||||
- 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)
|
||||
@@ -75,6 +79,9 @@ dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
|
||||
## Docs
|
||||
|
||||
- `docs/plan.md` — full architecture and design spec
|
||||
- `docs/open.md` — verification checklist and improvement backlog
|
||||
- `docs/improvement-plan.md` — prioritized improvement items
|
||||
- `docs/open.md` — open verification items and remaining code TODOs (the only doc kept current besides the CLAUDE.md files)
|
||||
- `docs/plan.md` — original design spec (historical; tag-queue/schema.sql parts are outdated)
|
||||
- `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)
|
||||
- `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`.
|
||||
|
||||
@@ -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.
|
||||
@@ -1,5 +1,7 @@
|
||||
# ClaudeDo — Improvement Plan (Session 2026-04-13)
|
||||
|
||||
> **Hinweis (2026-06-09):** Historischer Snapshot — bewusst nicht nachgepflegt. U.a. erledigt/überholt: IP-1 (Auto-Reconnect ist implementiert), `schema.sql` → EF-Core-Migrations, `StatusBarViewModel` existiert nicht mehr (Connection-State lebt in `IslandsShellViewModel`), Tags sind Junction-Tabellen statt JSON-Spalten. Offene Punkte stehen in `open.md`.
|
||||
|
||||
Erfasst während manuellem Walkthrough der App. Priorisiert nach Schmerz/Aufwand.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# ClaudeDo Online Inbox — API Contract & VPS build prompt
|
||||
|
||||
Status: handoff doc. The **server side** (API + minimal web client) is built and deployed
|
||||
VPS-side by a separate Claude instance. This file is the source of truth for the contract
|
||||
both ends implement against. The desktop client in this repo is built to match it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Concept
|
||||
|
||||
ClaudeDo is a local desktop app that runs tasks autonomously via the Claude CLI; it is
|
||||
normally fully local (SQLite). The **Online Inbox** is an optional service that lets the
|
||||
single owner view their task lists and add new tasks from a phone/browser. The desktop app
|
||||
syncs against it.
|
||||
|
||||
**Governing rule:** the online store mirrors EXACTLY the desktop's `Idle` backlog — nothing
|
||||
else. A task is present online only while it is `Idle` on the desktop. The moment the user
|
||||
queues it locally, the desktop removes it from the online store. Running / WaitingForReview /
|
||||
Done / Failed / Cancelled tasks never appear online.
|
||||
|
||||
Sync directions (each one-way per entity → no conflict resolution needed):
|
||||
|
||||
- **Lists**: desktop → online only. Desktop is the source of truth (full-replace catalog).
|
||||
- **Idle tasks**: desktop mirrors its Idle backlog up; the web can create new ones, which the
|
||||
desktop pulls down and then owns.
|
||||
|
||||
Single user today. Both the desktop and the web client authenticate as the **same Zitadel
|
||||
user**.
|
||||
|
||||
**Multi-user readiness (`ownerId`).** Each resource is owned by a Zitadel subject (`sub`).
|
||||
`RemoteList`, `RemoteTask`, and `MirrorTask` carry an optional `ownerId` field. The desktop
|
||||
stamps its own `sub` (decoded from the access token) onto everything it pushes, and
|
||||
defensively ignores any pulled task whose `ownerId` is set to a *different* user; an absent
|
||||
`ownerId` is treated as unowned/legacy and still syncs. This keeps the contract ready for
|
||||
multiple users **without enforcing isolation client-side** — the server remains the
|
||||
authority that scopes every request by the token's `sub`. When the server goes multi-user it
|
||||
should partition all rows by owner and ignore (or validate) the client-supplied `ownerId`.
|
||||
|
||||
**Access control (as of 2026-06-10).** Access is granted by assigning the **"user" project
|
||||
role** in the Zitadel project "ClaudeDo" (id `376787351902355727`, issuer
|
||||
`https://auth.kuns.dev`) — there is no app-side allowlist (the former `ALLOWED_USER_IDS`
|
||||
env var is gone). The access token carries the role in the claim
|
||||
`urn:zitadel:iam:org:project:roles` (or the project-scoped variant
|
||||
`urn:zitadel:iam:org:project:376787351902355727:roles`), an object keyed by role key, e.g.
|
||||
`{ "user": { "<orgId>": "<orgDomain>" } }`. The desktop OIDC client
|
||||
(id `376787352137302287`) has `accessTokenRoleAssertion` enabled, so any token issued
|
||||
after login/refresh includes the claim automatically — no extra scopes are needed.
|
||||
Granting/revoking access is purely a Zitadel role grant, nothing app-side.
|
||||
|
||||
## 2. Idle backlog definition (desktop side)
|
||||
|
||||
The desktop mirrors only "real" backlog items, not planning internals:
|
||||
|
||||
- `Status == Idle`
|
||||
- `ParentTaskId == null` (no planning/improvement children)
|
||||
- `PlanningPhase == None`
|
||||
- `BlockedByTaskId == null`
|
||||
|
||||
## 3. Data model (Postgres)
|
||||
|
||||
```
|
||||
lists
|
||||
id text primary key -- GUID supplied by the desktop; reuse verbatim
|
||||
name text not null
|
||||
updated_at timestamptz not null default now()
|
||||
|
||||
tasks
|
||||
id text primary key -- GUID; SHARED id space (see below)
|
||||
list_id text not null references lists(id) on delete cascade
|
||||
title text not null
|
||||
description text
|
||||
imported boolean not null default false -- false = web-created, awaiting desktop pull
|
||||
-- true = desktop-owned (mirrored or handed off)
|
||||
created_at timestamptz not null default now()
|
||||
updated_at timestamptz not null default now()
|
||||
```
|
||||
|
||||
**Shared GUID id space.** Web-created tasks get a server-generated GUID; the desktop imports
|
||||
under that SAME id, so it never duplicates. Desktop-mirrored tasks arrive with their own GUID.
|
||||
All task writes are idempotent upserts keyed on id.
|
||||
|
||||
**`imported` flag = ownership.**
|
||||
- Web `POST /tasks` inserts `imported=false`.
|
||||
- Desktop pulls `imported=false`, creates the task locally (reusing the id), then `POST
|
||||
/tasks/{id}/imported` flips it to `true`. From then on the task belongs to the desktop
|
||||
mirror.
|
||||
- `PUT /tasks/mirror` only ever inserts/updates/deletes within the `imported=true` partition.
|
||||
It never touches `imported=false` rows (those are pending handoff).
|
||||
|
||||
## 4. Endpoints
|
||||
|
||||
All endpoints require a valid Zitadel access token (`Authorization: Bearer <token>`) that
|
||||
carries the **"user" project role** (see §1). Missing/invalid/expired token, or a valid
|
||||
token without the role → `401`. No anonymous access (imported tasks can trigger code
|
||||
execution on the user's machine). The desktop client treats a `401` as: force a
|
||||
refresh-token exchange and retry once; if a freshly issued token is still rejected, it
|
||||
surfaces "missing 'user' role in Zitadel" and pauses sync until the user signs in again.
|
||||
|
||||
> **Auth (VPS/.NET):** use the in-house `KunsZitadel` nuget package (feed
|
||||
> `https://git.kuns.dev/api/packages/kuns/nuget/index.json`) — call `AddKunsZitadel(...)`
|
||||
> with the Zitadel authority/audience/client id to wire `JwtBearer` validation + CORS for
|
||||
> the web client origin. (`KunsZitadel` is server-side token *validation* only; the desktop
|
||||
> client acquires tokens via its own OIDC flow.)
|
||||
|
||||
| Method & path | Caller | Body | Response |
|
||||
|---|---|---|---|
|
||||
| `PUT /lists` | desktop | `[{ "id", "name", "ownerId"? }]` — the FULL catalog | `200` |
|
||||
| `GET /lists` | web | — | `200 [{ "id", "name", "ownerId"? }]` |
|
||||
| `GET /lists/{id}/tasks` | web | — | `200` tasks in that list (`404` if list unknown) |
|
||||
| `POST /tasks` | web | `{ "title", "description"?, "listId" }` | `201` created task incl. `id` |
|
||||
| `GET /tasks?imported=false` | desktop | — | `200 [{ "id","listId","title","description","createdAt","ownerId"? }]` |
|
||||
| `POST /tasks/{id}/imported` | desktop | — | `200` (`404` if unknown) |
|
||||
| `PUT /tasks/mirror` | desktop | `[{ "id","listId","title","description","ownerId"? }]` — full Idle set | `200` |
|
||||
|
||||
`ownerId` (optional, see §1) is the Zitadel `sub` of the owner. The desktop sends it on push
|
||||
and ignores pulled tasks owned by a different user; the server should derive/validate it from
|
||||
the token rather than trust the client value.
|
||||
|
||||
Semantics:
|
||||
|
||||
- **`PUT /lists`** — full replace: upsert all supplied, DELETE any list not in the payload
|
||||
(cascades its tasks). Idempotent.
|
||||
- **`POST /tasks`** — `listId` must exist (`400`/`404` otherwise). Server generates the id.
|
||||
- **`PUT /tasks/mirror`** — full replace of the `imported=true` partition: upsert every task
|
||||
in the payload (insert with `imported=true`, or update), and DELETE any `imported=true`
|
||||
task whose id is not in the payload. `imported=false` rows are untouched. Idempotent.
|
||||
- All task ids are client-trusted within the shared space; the server never rewrites an id.
|
||||
|
||||
## 5. Reconcile loop (desktop, runs each poll cycle)
|
||||
|
||||
```
|
||||
1. PULL: GET /tasks?imported=false
|
||||
for each: if no local task with that id → create local TaskEntity
|
||||
{ Id = remote.id, ListId = remote.listId, Title, Description,
|
||||
Status = Idle, CreatedBy = "online" }
|
||||
(skip + log if remote.listId has no local list)
|
||||
then POST /tasks/{id}/imported
|
||||
2. PUSH LISTS: PUT /lists with the full local catalog [{id, name}]
|
||||
3. PUSH TASKS: PUT /tasks/mirror with the current local Idle backlog set (§2)
|
||||
```
|
||||
|
||||
Ordering matters: pull+import+flag first, so the just-imported tasks are part of the local
|
||||
Idle set computed in step 3 and survive the mirror replace.
|
||||
|
||||
## 6. Minimal web client
|
||||
|
||||
Integrate into the existing Nuxt app at claudedo.kuns.dev if present; else a minimal page.
|
||||
|
||||
- Zitadel login.
|
||||
- Show lists (`GET /lists`); select one to see its Idle tasks (`GET /lists/{id}/tasks`).
|
||||
- Add-task form → `POST /tasks`.
|
||||
- Mobile-first (main use: jotting ideas from a phone).
|
||||
- **Create + read only.** No editing, reordering, status changes, or deletes.
|
||||
|
||||
## 7. Security
|
||||
|
||||
- Every route auth-gated (`401` on bad token); only static assets / login are public.
|
||||
- Validate `listId` on task creation; parameterized queries only.
|
||||
- CORS restricted to the web client origin.
|
||||
- Don't log task titles/descriptions at info level (user content).
|
||||
|
||||
## 8. Deliverables from the VPS build
|
||||
|
||||
Report back so the desktop can be configured:
|
||||
|
||||
1. **API base URL.**
|
||||
2. **Zitadel app/client config the desktop must use**: issuer/authority, client id, scopes,
|
||||
and the OAuth flow to use for a desktop app (device-code or auth-code + PKCE), plus how
|
||||
refresh tokens are issued.
|
||||
3. Any env vars / README.
|
||||
|
||||
Out of scope server-side: task execution (the desktop runs Claude), any task state other
|
||||
than the Idle mirror, multi-user / sharing / notifications.
|
||||
+61
-15
@@ -1,30 +1,76 @@
|
||||
# ClaudeDo — Offene Punkte
|
||||
|
||||
Stand: 2026-06-04. **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:**
|
||||
- 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").
|
||||
- **Worker-Autostart am Gerät:** Logoff/Logon-Autostart, Update-Pfad, Uninstall entfernt die Startup-`.lnk`.
|
||||
## UX / Nits (verifiziert 2026-07-24)
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## Feature-Wünsche (aus der Session)
|
||||
|
||||
- **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).
|
||||
|
||||
## Sichtprüfung offen (2026-07-27, Batch „Claude do"-Liste)
|
||||
|
||||
Alles gebaut + unit-getestet, aber **nicht visuell verifiziert** — Mika prüft:
|
||||
|
||||
- **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.
|
||||
|
||||
Offene Entscheidungen dazu:
|
||||
|
||||
- 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 nur noch Fallback für ein Modell ohne Preset-Zeile und hat keinen Editor mehr. Spalte könnte später entfallen.
|
||||
|
||||
## 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).
|
||||
|
||||
---
|
||||
|
||||
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||
|
||||
- **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`.
|
||||
- **`architecture.md` / ADRs** — die per-Projekt-`CLAUDE.md`-Dateien sind die lebende Doku; ADRs lohnen solo nicht.
|
||||
- **Task-Mailbox-Integration** — geparkt; das generische `mcp__mailbox__*`-Plugin reicht (Begründung in `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).
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ToDo-App mit autonomem Agent-Worker — Design
|
||||
|
||||
> **Hinweis (2026-06-09):** Historisches Design-Dokument vom Projektstart — bewusst nicht nachgepflegt. Überholt sind insbesondere: die Tag-basierte Queue (entfernt; der Picker nutzt `Status=Queued` + `BlockedByTaskId IS NULL`), `schema.sql` (Schema läuft über EF-Core-Migrations) und das Projektlayout (inzwischen sechs Testprojekte). Lebende Doku sind die `CLAUDE.md`-Dateien pro Projekt.
|
||||
|
||||
## Context
|
||||
|
||||
Ziel: eine persönliche ToDo-App als Desktop-Anwendung, in der mehrere Listen verwaltet werden können. Ein Teil der Tasks soll autonom von Claude abgearbeitet werden (z.B. Recherche, Code-Aufgaben, Notizen-Verarbeitung). Die Autonomie läuft in einem getrennten Hintergrund-Prozess, damit die UI davon entkoppelt bleibt.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Plan: Per-task model override via MCP + cheapest-model prompt guidance
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-09-per-task-model-override-design.md`
|
||||
|
||||
TDD, one focused commit per task. Build with `-c Release` per project; run
|
||||
`ClaudeDo.Worker.Tests` (and `Data.Tests` if touched).
|
||||
|
||||
## Task 1 — ModelRegistry: cost ordering + alias validation
|
||||
|
||||
- Add `ByCostAscending = ["haiku","sonnet","opus"]`.
|
||||
- Add `string? NormalizeAlias(string? model)`: trim; null/blank → null;
|
||||
case-insensitive match against `Aliases` → canonical lowercase; else throw
|
||||
`ArgumentException($"Unknown model '{model}'. Allowed: {join(Aliases)}.")`.
|
||||
- Tests (Data.Tests): "sonnet"/"OPUS"/" haiku " → normalized; ""/null/" " →
|
||||
null; "gpt4" → throws.
|
||||
|
||||
## Task 2 — CreateChildAsync accepts model
|
||||
|
||||
- `TaskRepository.CreateChildAsync`: add `string? model = null` (before the
|
||||
trailing `CancellationToken ct = default`); set
|
||||
`child.Model = ModelRegistry.NormalizeAlias(model)`.
|
||||
- Update the two existing callers to compile (named pass-through added in
|
||||
Tasks 3–4; keep default null here).
|
||||
|
||||
## Task 3 — Planning + improvement MCP tools forward model
|
||||
|
||||
- `PlanningMcpService.CreateChildTask`: add `string? model` param after
|
||||
`commitType`; pass to `CreateChildAsync`. Extend `[Description]` to document
|
||||
the model arg (haiku/sonnet/opus; cheapest capable).
|
||||
- `TaskRunMcpService.SuggestImprovement`: add `string? model` param after
|
||||
`description`; pass to `CreateChildAsync`. Extend `[Description]`.
|
||||
- Tests: each tool persists the model; invalid value throws.
|
||||
|
||||
## Task 4 — External AddTask forwards model
|
||||
|
||||
- `ExternalMcpService.AddTask`: add `string? model = null` param (before the
|
||||
trailing `CancellationToken`); `entity.Model = ModelRegistry.NormalizeAlias(model)`.
|
||||
Extend `[Description]`.
|
||||
- Test: AddTask persists model; invalid value rejected.
|
||||
|
||||
## Task 5 — Prompt guidance
|
||||
|
||||
- `PromptFiles.PlanningSystemDefault`: add a short paragraph — assign each
|
||||
subtask the cheapest model that does it well, with ordering haiku < sonnet <
|
||||
opus and the heuristic; pass it as `CreateChildTask(model=...)`.
|
||||
- `PromptFiles.SystemDefault` Out-of-scope section: when filing via
|
||||
`SuggestImprovement`, pass the cheapest capable `model`.
|
||||
- `PromptFiles.ImprovementChildDefault`: one-line minimality reminder.
|
||||
- No test (static prompt text); verify build only.
|
||||
|
||||
## Task 6 — Verify
|
||||
|
||||
- Build App + Worker `-c Release`; run Worker.Tests + Data.Tests.
|
||||
- Update `ClaudeDo.Worker/CLAUDE.md` (ConfigMcpTools/creation-tool notes) and
|
||||
`ClaudeDo.Data/CLAUDE.md` (ModelRegistry) if needed.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Online Inbox — implementation plan
|
||||
|
||||
Date: 2026-06-10
|
||||
Spec: `docs/superpowers/specs/2026-06-10-online-inbox-design.md`
|
||||
Contract: `docs/online-inbox-api-contract.md`
|
||||
|
||||
TDD, one commit per task, Conventional Commits. Build with `-c Release` per CLAUDE.md.
|
||||
|
||||
## Phase 1 — Worker sync engine (buildable now, no Zitadel package needed)
|
||||
|
||||
### Task 1 — Config
|
||||
- Add `OnlineInboxConfig` + nested `ZitadelClientConfig` records.
|
||||
- Add `online_inbox` (`OnlineInbox`) property to `WorkerConfig`; default `enabled=false`.
|
||||
- `Load` leaves it untouched when absent (defaults = disabled).
|
||||
- Test: missing section → disabled defaults; populated section round-trips.
|
||||
|
||||
### Task 2 — DTOs + Idle-backlog helper
|
||||
- `Online/Dtos.cs`: `RemoteList(Id, Name)`, `RemoteTask(Id, ListId, Title, Description, CreatedAt)`,
|
||||
`MirrorTask(Id, ListId, Title, Description)`.
|
||||
- `Online/OnlineBacklog.cs`: `static Task<List<MirrorTask>> CurrentAsync(TaskRepository/ctx)` +
|
||||
the filter predicate (Idle, no parent, PlanningPhase None, BlockedBy null).
|
||||
- Test the filter against real SQLite seeded with mixed tasks.
|
||||
|
||||
### Task 3 — Auth abstraction + token store
|
||||
- `Online/Interfaces/IOnlineAuthProvider.cs`.
|
||||
- `Online/OnlineTokenStore.cs`: DPAPI CurrentUser persistence at `~/.todo-app/online-inbox.token`;
|
||||
`Save(refreshToken)`, `Read()`, `Clear()`. (Windows-only encryption; thin + guarded.)
|
||||
- A trivial `StaticTokenAuthProvider` (returns a configured token or null) for tests + as the
|
||||
temporary default until Zitadel is wired.
|
||||
- Test: token store round-trip (Windows); static provider returns/omits token.
|
||||
|
||||
### Task 4 — API client
|
||||
- `Online/IOnlineInboxApi.cs` + `Online/OnlineInboxApiClient.cs` (typed `HttpClient`).
|
||||
- Attaches `Authorization: Bearer` from `IOnlineAuthProvider`; refuses non-HTTPS non-loopback
|
||||
base URLs; throws a typed `OnlineInboxException` on non-2xx.
|
||||
- Test with a stubbed `HttpMessageHandler`: each method hits the right path/verb/body; 401
|
||||
surfaces; bearer attached.
|
||||
|
||||
### Task 5 — Sync service
|
||||
- `Online/OnlineSyncService.cs` (`BackgroundService`) implementing the §5 reconcile loop.
|
||||
- DI: register only when `enabled`; resolve repos per-cycle via a scope.
|
||||
- Per-cycle try/catch + structured logging; skip when no token; unknown-list skip.
|
||||
- Test against a **fake `IOnlineInboxApi`** + real SQLite: pull→import→flag creates local Idle
|
||||
tasks; mirror payload == Idle backlog; lists pushed; unknown list skipped & not flagged;
|
||||
disabled/no-token = no api calls.
|
||||
|
||||
### Task 6 — Wire-up + docs
|
||||
- Register the stack in `Program.cs` behind the enabled flag.
|
||||
- Update `src/ClaudeDo.Worker/CLAUDE.md` (new `Online/` area) and `src/ClaudeDo.Worker/Config`
|
||||
notes. Add `online_inbox` to the config section.
|
||||
|
||||
## Phase 2 — UI + real auth (AFTER the VPS reports client config)
|
||||
|
||||
### Task 7 — Hub + config plumbing
|
||||
- Hub: `GetOnlineInboxConfig` / `SetOnlineInboxConfig` / `SetOnlineInboxAuth(refreshToken)` /
|
||||
`ClearOnlineInboxAuth`. Update `IWorkerClient` + `WorkerClient` + test fakes (both test
|
||||
projects — see the IWorkerClient-fakes memory).
|
||||
|
||||
### Task 8 — Settings UI
|
||||
- "Online Inbox" section in `SettingsModalViewModel`: enable toggle, base URL, Sign in/out,
|
||||
status. Localized keys in en.json + de.json (parity).
|
||||
- Visual verification = manual (flag it).
|
||||
|
||||
### Task 9 — ZitadelAuthProvider
|
||||
- Add the Zitadel package reference; implement `ZitadelAuthProvider` (refresh-token → access
|
||||
token, cached to expiry) using the reported authority/client-id/flow.
|
||||
- Swap it in for `StaticTokenAuthProvider` in DI when enabled.
|
||||
- Manual smoke against the live VPS API (tracked, not an automated test).
|
||||
|
||||
## Notes
|
||||
- No real network / no real Zitadel / no real Claude in any automated test.
|
||||
- Stage files by explicit path in subagents; sonnet model; build+test+commit by the orchestrator.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Feature unification — phased plan
|
||||
|
||||
Date: 2026-06-19
|
||||
Design: `docs/superpowers/specs/2026-06-19-feature-unification-design.md`
|
||||
|
||||
Six slices, sequenced cheapest/lowest-risk first. Each ends green
|
||||
(`dotnet build -c Release` + the touched test project) and is independently
|
||||
committable. Phases 0–1 are detailed here; 2–5 are scoped, and each gets its own
|
||||
`docs/superpowers/plans/2026-06-19-unify-<slice>.md` when picked up (per the
|
||||
2026-06-05 layer-A/B/C convention). Build per-csproj (`-c Release`) — `.slnx` needs
|
||||
.NET 9 and a running Worker locks `Debug`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Groundwork (Bucket C). No UX change.
|
||||
|
||||
**0a. Delete the dead hunks conflict API (C1).**
|
||||
- Remove `TaskMergeService.GetConflictsAsync` + the `MergeConflicts`/`ConflictFileContent` records it returns (`src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs:250`) if unused elsewhere.
|
||||
- Remove `WorkerHub.GetMergeConflicts` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs:378`) + `MergeConflictsDto`/`ConflictFileDto`/`ConflictHunkDto` if unused.
|
||||
- Remove `WorkerClient`'s `"GetMergeConflicts"` invoke (`src/ClaudeDo.Ui/Services/WorkerClient.cs:276`) + the `IWorkerClient` member + every fake override (`tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `TasksIslandViewModelPlanningTests.cs`, others — grep `GetMergeConflicts`).
|
||||
- Delete `TaskMergeServiceTests.cs:672` `GetConflictsAsync_AfterConflictMerge_ReturnsOursAndTheirs`.
|
||||
- Verify with grep first: `GetConflictsAsync` and `GetMergeConflicts` have **no** callers outside this chain + tests.
|
||||
- Acceptance: Worker + Ui build; Worker.Tests + Ui.Tests green; `GetMergeConflictDocuments` path untouched.
|
||||
|
||||
**0b. Single task-creation path (C2).**
|
||||
- Identify the path MCP `ExternalMcpService.AddTask` uses; expose a thin creation method (repository or a small `TaskCreationService`) that applies the same defaults (ListId, SortOrder, CreatedAt).
|
||||
- Re-point `TasksIslandViewModel.AddAsync` at it instead of `db.Tasks.Add` direct EF.
|
||||
- Acceptance: quick-add still works; one creation path; Ui.Tests + Worker.Tests green.
|
||||
|
||||
**0c. Prune stale worktrees (C3).**
|
||||
- `git worktree list`; remove the orphaned `.claude/worktrees/*` entries (confirm each is unwanted with Mika before `git worktree remove`).
|
||||
- Acceptance: only intended worktrees remain; no tracked files change.
|
||||
|
||||
> C4 (naming alignment) intentionally NOT in this phase — see design.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — DialogService (B3–B5). Low–medium.
|
||||
|
||||
**Goal:** one `IDialogService` replaces the scattered `Show*` Func seams and the
|
||||
duplicate open-commands.
|
||||
|
||||
- New `IDialogService` (Ui/Services) with typed methods: `OpenListSettings(ListNavItemViewModel)`, `OpenRepoImport()`, `OpenWorktreesOverview(string? listId)`, `OpenWeeklyReport()`, `OpenAbout()`, `OpenWorkerConnectionHelp()`. Implementation owns the factories + `ModalShell`/TCS wiring currently in `MainWindow.axaml.cs` + `IslandsShellViewModel.cs:59-71`.
|
||||
- Inject it into `ListsIslandViewModel`, `TasksIslandViewModel`, `IslandsShellViewModel`. Collapse the three List-Settings doors (Lists context menu, Tasks header, shell bridge `IslandsShellViewModel.cs:190-194`) to one `dialogs.OpenListSettings(row)` call; same for Repo Import (2→1) and Worktrees Overview (2→1, keep the `listId?` param for global-vs-per-list).
|
||||
- Keep `ModalShell`/TCS dialog pattern; this only centralizes *opening*.
|
||||
- Update fakes/ctors per the IWorkerClient-fakes hazard (ctor changes ripple to Ui.Tests).
|
||||
- Acceptance: every dialog opens via one method; no duplicate open-commands; Ui.Tests green; visual gap flagged (open each dialog from each former door).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — MergeCoordinator (B1). Medium.
|
||||
|
||||
**Goal:** delete the five `RequestConflictResolution` seams; one coordinator.
|
||||
|
||||
- New `IMergeCoordinator` (Ui) `MergeAsync(taskId, targetBranch)` = the body of `IslandsShellViewModel.RequestConflictResolutionAsync` (`:49`) plus the "open MergeModal → on conflict open resolver" flow currently split across `MergeModalViewModel:108` and `DiffModalViewModel:103`.
|
||||
- Remove the `Func<string,string,Task>? RequestConflictResolution` from `WorktreesOverviewModalViewModel:83`, `DiffModalViewModel:75`, `MergeModalViewModel:33`, `MergeSectionViewModel:51`, and the `DetailsIslandViewModel:347` delegate; inject the coordinator instead.
|
||||
- Re-point doors: review Approve, Diff Merge button, WorktreesOverview single + batch (`:331`), Details merge section.
|
||||
- Update seam tests (`WorktreesOverviewBatchMergeTests.cs:145`, `DetailsIslandConflictSeamTests.cs:84`) to assert via the coordinator.
|
||||
- Acceptance: one merge entry API; resolver still opens for single-task AND planning conflict; Ui.Tests green; visual gap flagged (force a conflict from Approve and from the Diff Merge button).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — WorktreeActions (A3). Medium.
|
||||
|
||||
**Goal:** one per-task worktree-actions VM reused by overview rows + Details.
|
||||
|
||||
- New `WorktreeActionsViewModel(taskId)` with Merge/Diff/Discard/Keep/ForceRemove over `IWorkerClient` (uses the Phase-2 coordinator for Merge, the Phase-5 viewer for Diff — until then, current calls).
|
||||
- `WorktreesOverviewModalViewModel` rows compose one each; `MergeSectionViewModel` hosts one for the active task. Remove the duplicated commands.
|
||||
- Acceptance: both surfaces drive the same VM; Ui.Tests green; visual gap flagged.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — AgentConfigEditor (A2). Medium.
|
||||
|
||||
**Goal:** one config editor for Global | List | Task scope.
|
||||
|
||||
- New `AgentConfigEditorViewModel(scope)` over `InheritanceResolver` exposing Model/SystemPrompt/AgentPath/MaxTurns + reset commands + `InheritedBadge` state; persists via the scope's hub method (`UpdateListConfig` / `UpdateTaskAgentSettings` / app settings).
|
||||
- Embed in `SettingsModalViewModel`, `ListSettingsModalViewModel`, and the Details `AgentSettingsSectionViewModel` host; delete the duplicated field/reset logic.
|
||||
- Acceptance: identical editor in all three scopes; Localization parity; Ui.Tests green; visual gap flagged.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — DiffViewer (A1 + B2). High; last.
|
||||
|
||||
**Goal:** one diff component replaces DiffModal + WorktreeModal + PlanningDiff.
|
||||
|
||||
- New `DiffViewerViewModel` with `DiffSource` enum/abstraction (`DirtyWorktree | BranchVsBase | CommitRange | PlanningAggregate | IntegrationBranch`) and an optional file-tree pane (port `WorktreeModal`'s tree + Avalonia-12 selection workaround); reuse `UnifiedDiffParser` + `DiffLinesView`; keep PlanningDiff's combined-mode toggle as a source switch.
|
||||
- Re-point all B2 doors to open it with the right source. Remove the three old VMs/views.
|
||||
- Update `DiffModalViewModelTests`, `PlanningDiffViewModelTests`.
|
||||
- Acceptance: every diff door opens the one viewer; whole-unified AND file-tree layouts work; Ui.Tests green; visual gap flagged (worktree-dirty, post-merge commit-range, planning per-subtask + integration).
|
||||
|
||||
---
|
||||
|
||||
## Sequencing rationale
|
||||
|
||||
0 (delete/no-UX) → 1 (isolated, unblocks nothing but cheap) → 2 (coordinator; 3 & 5
|
||||
lean on it for Merge/Diff) → 3 → 4 (independent) → 5 (biggest, most UX-sensitive,
|
||||
benefits from 2's coordinator). Stop after any phase and the app is shippable.
|
||||
|
||||
## Per-phase commits
|
||||
|
||||
Conventional Commits, one per phase (or per sub-step in Phase 0): e.g.
|
||||
`refactor(merge): single MergeCoordinator replaces 5 conflict seams`. Stage by path
|
||||
(never `git add -A` — concurrent sessions). Commit the spec + this plan first.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Plan: Rider-style 3-pane merge editor
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-19-rider-merge-editor-design.md`
|
||||
|
||||
TDD, one focused commit per task (Conventional Commits, `feat(merge): …`).
|
||||
Build with `-c Release` per project (a running Worker locks `Debug`).
|
||||
Run `ClaudeDo.Ui.Tests` (and `Localization.Tests` for Task 6). No real `claude` CLI in tests.
|
||||
Stage ONLY the files each task touches, by explicit path (parallel sessions leave WIP).
|
||||
Backend + seam stay unchanged. Implementer/reviewer subagents use **sonnet**.
|
||||
|
||||
## Task 1 — VM: active-file model + 3-pane reconstruction + readout
|
||||
|
||||
`ConflictResolverViewModel` / `ConflictModels.cs`, additive (seam untouched).
|
||||
|
||||
- Add `ActiveFile` (`MergeFile?`), `SelectFileCommand(MergeFile)`, default to first file
|
||||
after load. Keep `Files`, `Current`/`CurrentIndex`/`Next`/`Previous` (focused conflict
|
||||
for the header arrows), `CanContinue`, binary guard, planning routing — all unchanged.
|
||||
- Add computed, per `ActiveFile`:
|
||||
- `ActiveOursText` = concat(stable.Text | conflict.Ours)
|
||||
- `ActiveTheirsText` = concat(stable.Text | conflict.Theirs)
|
||||
- `ActiveResultText` = concat(stable.Text | conflict.Resolution ?? conflict.Ours)
|
||||
- `ActiveConflicts` = ordered descriptors (block + segment index) for the view.
|
||||
- `PositionText` → `"{conflicts} conflicts · {resolved} resolved"` for the active file;
|
||||
keep `CanContinue` = every file resolved AND no binary.
|
||||
- Switching files raises a change event the view listens to (reuse/extend
|
||||
`CurrentChanged` → e.g. `ActiveFileChanged`).
|
||||
- Tests (Ui.Tests): reconstruction text for ours/theirs/result (result seeds unresolved
|
||||
with Ours); resolving a block updates `ActiveResultText` + readout; switching files
|
||||
preserves each block's `Resolution`; `CanContinue` blocks until all files resolved;
|
||||
binary file still blocks. Keep all existing tests green.
|
||||
|
||||
## Task 2 — View: 3-pane AXAML shell + document assembly + synced scroll
|
||||
|
||||
`Views/Conflicts/ConflictResolverView.axaml(.cs)`. Visual — verified by running.
|
||||
|
||||
- Replace AXAML: ModalShell host kept; header row (◀/▶ focus arrows bound to
|
||||
Previous/Next, file switcher `ItemsControl`/`ComboBox` over `Files` bound to
|
||||
`SelectFileCommand`, right-aligned `PositionText`); `Grid ColumnDefinitions="*,*,*"`
|
||||
of three bordered panes with headers **Ours · current (merge target)** /
|
||||
**Result** / **Theirs · incoming (task)** (drop Base); footer Continue
|
||||
(`IsEnabled=CanContinue`) / Abort; binary banner (kept); `Escape`→Abort (kept).
|
||||
- Code-behind: build three `TextDocument`s from `ActiveFile` segments, recording each
|
||||
conflict's start line + line count per document; install TextMate per pane by file
|
||||
extension; rebuild on `ActiveFileChanged`; Ours/Theirs `IsReadOnly=true`.
|
||||
- Proportional synced vertical scroll across the three panes (re-entrancy guard).
|
||||
- Push Result edits back to the active block `Resolution` (refined in Task 4).
|
||||
|
||||
## Task 3 — Result pane: read-only stable, editable conflicts
|
||||
|
||||
`ConflictResolverView.axaml.cs` + a small `IReadOnlySectionProvider` helper.
|
||||
|
||||
- Track each conflict's result span in a `TextSegmentCollection<…>` over the Result
|
||||
document (anchors auto-adjust on edit).
|
||||
- `IReadOnlySectionProvider`: `CanInsert` only strictly inside a conflict span;
|
||||
`GetDeletableSegments` intersects with conflict spans only. Stable text becomes
|
||||
immutable; conflict regions stay editable.
|
||||
- Editing inside a conflict span writes the span text back to the block `Resolution`
|
||||
and flips it resolved (updates readout + `CanContinue`).
|
||||
|
||||
## Task 4 — Color blocks (IBackgroundRenderer) + accept overlay
|
||||
|
||||
`ConflictResolverView.axaml.cs` + renderer/overlay helpers.
|
||||
|
||||
- `IBackgroundRenderer` per pane: unresolved conflict = red (Blood tint), resolved =
|
||||
green/muted, Ours side = Moss tint, Theirs side = Accent tint — driven by recorded
|
||||
spans + block `IsResolved`.
|
||||
- Between-pane overlay Canvas (Ours|Result and Result|Theirs): `›` accept-ours / `‹`
|
||||
accept-theirs + `✕` dismiss per conflict, positioned at the block's `TextView` visual
|
||||
top, recomputed on scroll/resize. Click → `block.AcceptOurs/AcceptTheirs` and replace
|
||||
the tracked Result span; resolved blocks recolor.
|
||||
|
||||
## Task 5 — Polish: readout, focus arrows scroll-to-conflict, resolved styling
|
||||
|
||||
- ◀/▶ arrows move `Current` and scroll all three panes to that conflict.
|
||||
- `M conflicts · K resolved` live readout; Continue tooltip/hint when blocked.
|
||||
- Resolved conflict recolors and drops its accept overlay; unresolved stays red.
|
||||
(Fold into Task 4 if small.)
|
||||
|
||||
## Task 6 — Localization + tokens
|
||||
|
||||
- Add `conflictResolver.*` keys (pane headers, readout, accept tooltips, hints) to
|
||||
`locales/en.json` AND `locales/de.json` (keep key parity).
|
||||
- Add Tokens.axaml color tokens only if a needed conflict/resolved shade is missing.
|
||||
- Run Localization.Tests (parity) + a quick scan for hard-coded strings in the view.
|
||||
|
||||
## Task 7 — Verify
|
||||
|
||||
- Build `ClaudeDo.App` + `ClaudeDo.Ui` `-c Release`; run `Ui.Tests` + `Localization.Tests`.
|
||||
- Update `src/ClaudeDo.Ui/CLAUDE.md` (Planning/Conflicts paragraph → new 3-pane editor).
|
||||
- **Visual verification gap (flag to Mika):** run the app, trigger a real conflict
|
||||
(single-task approve + planning unit-merge) and confirm panes/colors/accept/scroll/
|
||||
gating/binary render correctly — cannot be asserted in tests.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Phase 4 — AgentConfigEditor (A2)
|
||||
|
||||
Date: 2026-06-23 (picked up after reordering Phase 3 ↔ 4)
|
||||
Umbrella: `docs/superpowers/plans/2026-06-19-feature-unification-plan.md`
|
||||
Design: `docs/superpowers/specs/2026-06-19-feature-unification-design.md` (A2)
|
||||
|
||||
## Reordering note
|
||||
|
||||
Phase 3 (WorktreeActions) was deferred. Its premise — overview rows and the Details
|
||||
merge section each owning duplicate worktree commands — only half-holds: Details has
|
||||
no Discard/Keep/ForceRemove, and the two Diff doors open different VMs (`WorktreeModal`
|
||||
vs `DiffModal`) that only Phase 5 unifies. So Phase 3's clean form depends on Phase 5
|
||||
(Diff) and a fuller MergeCoordinator (Merge); doing it now would build throwaway
|
||||
per-surface delegates. **Phase 3 is folded into Phase 5.** Phase 4 (independent, clean
|
||||
dedup) runs now.
|
||||
|
||||
## Scope decision: List + Task only (global left as-is)
|
||||
|
||||
The design names three scopes (Global | List | Task). Verified against the tree on
|
||||
2026-06-23, only **List and Task genuinely duplicate**:
|
||||
|
||||
- **List** (`ListSettingsModalViewModel`, "AGENT" section): Model / MaxTurns /
|
||||
SystemPrompt / AgentFile, each with `InheritedBadge` + `↺` reset; 2-tier
|
||||
(list→global) badges computed with inline logic (does **not** use the existing
|
||||
`InheritanceResolver.ResolveList` — which is currently dead code); explicit Save.
|
||||
- **Task** (`AgentSettingsSectionViewModel`, TaskHeaderBar gear flyout): same four
|
||||
fields; 3-tier (task→list→global) badges via `InheritanceResolver.Resolve`;
|
||||
`EffectiveMaxTurns` + `EffectiveSystemPromptHint`; `IsRunning` gate; debounced
|
||||
auto-save.
|
||||
|
||||
**Global** (`GeneralSettingsTabViewModel`, Settings → General) is the root: no
|
||||
inheritance, no badges, no agent file, no reset — three plain controls (model combo,
|
||||
max-turns numeric, instructions textbox) plus a global-only PermissionMode, interleaved
|
||||
with unrelated settings (Language, parallelism, report paths, standup weekday) and
|
||||
saved batched into one `AppSettingsDto` via the modal Save. Embedding the shared editor
|
||||
there buys ~3 plain fields at the cost of a degenerate no-badges/no-agent/no-reset mode
|
||||
plus surgery on the settings save path and a relayout of the most settings-dense view.
|
||||
**Not worth it — global stays as-is.** (Confirmed with Mika 2026-06-23.)
|
||||
|
||||
The real maintenance hazard is the **VM logic** (two copies of badge/reset/inheritance
|
||||
that already drifted), and the **view** (3 of 4 field blocks are pixel-identical). Both
|
||||
collapse cleanly for List+Task.
|
||||
|
||||
## Target
|
||||
|
||||
One `AgentConfigEditorViewModel` + one `AgentConfigEditor` UserControl, instantiated
|
||||
per surface with a scope. The two host VMs keep only their non-agent concerns and host
|
||||
the editor as a child.
|
||||
|
||||
### `ViewModels/Agent/AgentConfigEditorViewModel.cs` (new)
|
||||
|
||||
- `enum AgentConfigScope { List, Task }`
|
||||
- ctor `(IWorkerClient worker, AgentConfigScope scope)`
|
||||
- Unified bindable surface (single names both views bind to):
|
||||
`Model` (string?), `MaxTurns` (decimal?), `SystemPrompt` (string),
|
||||
`SelectedAgent` (AgentInfo?); `ModelOptions`, `Agents`;
|
||||
`ModelBadge`/`TurnsBadge`/`AgentBadge`, `ModelInheritedHint`/`TurnsInheritedHint`,
|
||||
`EffectiveSystemPromptHint`; `EffectiveMaxTurns` (int), `IsRunning`/`IsEnabled`.
|
||||
- Reset commands: `ResetModel`, `ResetTurns`, `ResetAgent`, `ResetAll`.
|
||||
- Badges via `InheritanceResolver`: scope==Task → `Resolve(own, list, global)`;
|
||||
scope==List → `ResolveList(own, global)` (adopts the dead method). One `BadgeFor`
|
||||
helper covers both (List scope never yields the `List` source).
|
||||
- Load: `LoadForListAsync(listId)` and `LoadForTaskAsync(TaskEntity entity)` — both
|
||||
pull agents + app-settings (global defaults); Task also pulls the list tier +
|
||||
`EffectiveSystemPromptHint`. Localizer-change re-badges (port the `Loc.LanguageChanged`
|
||||
handler + `IDisposable`).
|
||||
- Save: `SaveAsync()` is scope-aware — List builds `UpdateListConfigDto` →
|
||||
`UpdateListConfigAsync`; Task builds `UpdateTaskAgentSettingsDto` →
|
||||
`UpdateTaskAgentSettingsAsync`. Task scope also auto-saves debounced (300ms) on field
|
||||
changes; List does not (the modal Save button calls `SaveAsync`). `SaveAsync` is
|
||||
directly callable (tests bypass the debounce).
|
||||
- Task-only `Clear()` + `TaskId`.
|
||||
|
||||
### `Views/Controls/AgentConfigEditor.axaml` (+ .axaml.cs) (new)
|
||||
|
||||
- `x:DataType` = `AgentConfigEditorViewModel`; host sets `DataContext="{Binding Agent}"`.
|
||||
- The four field blocks (model/turns/systemprompt/agent) with `InheritedBadge` + `↺`
|
||||
reset, lifted verbatim from the existing two views (they already match). Agent combo
|
||||
shows Name + Description (both scopes; harmless for task). `EffectiveSystemPromptHint`
|
||||
line gated on non-empty (hides for List).
|
||||
- `StyledProperty<bool> ShowAgentBrowse` (default false). True → render the Browse
|
||||
button + path line; the browse file-picker code-behind lives here (moved from
|
||||
`ListSettingsModalView`).
|
||||
- Shared localization namespace `settings.agentEditor.*` (model/maxTurns/systemPrompt/
|
||||
agentFile/promptPrepended). Reset tooltip reuses `settings.inherit.resetToInherited`.
|
||||
|
||||
### Re-point hosts
|
||||
|
||||
- `ListSettingsModalViewModel`: drop the agent fields/badges/resets/option-lists; add
|
||||
`public AgentConfigEditorViewModel Agent { get; }` (scope=List). `LoadAsync` →
|
||||
`Agent.LoadForListAsync(listId)`. `SaveAsync` keeps `UpdateListAsync` (name/dir) and
|
||||
adds `await Agent.SaveAsync()`. Keep working-dir browse (`BrowseClicked`).
|
||||
- `ListSettingsModalView.axaml`: replace the AGENT section body with
|
||||
`<ctl:AgentConfigEditor DataContext="{Binding Agent}" ShowAgentBrowse="True"/>`; the
|
||||
section-header "Reset agent settings" button binds `Agent.ResetAllCommand`. Remove the
|
||||
agent browse code-behind (moved into the control).
|
||||
- `DetailsIslandViewModel`: `AgentSettings` becomes `AgentConfigEditorViewModel`
|
||||
(scope=Task). Preserve the call sites: ctor, `EffectiveMaxTurns`→`TurnsText`
|
||||
PropertyChanged hook, `IsRunning` push, `Dispose`, `Clear`, `TaskId`,
|
||||
`LoadForTaskAsync(entity, ct)`.
|
||||
- `TaskHeaderBar.axaml`: replace the flyout field blocks with
|
||||
`<ctl:AgentConfigEditor DataContext="{Binding AgentSettings}"/>` (ShowAgentBrowse=false).
|
||||
Keep the gear button + heading.
|
||||
- Delete `AgentSettingsSectionViewModel.cs`.
|
||||
|
||||
## Tests
|
||||
|
||||
- New `tests/ClaudeDo.Ui.Tests/ViewModels/AgentConfigEditorViewModelTests.cs`:
|
||||
- List scope: badges resolve override-vs-global; resets clear; `SaveAsync` builds the
|
||||
right `UpdateListConfigDto` (via `StubWorkerClient`).
|
||||
- Task scope: badges resolve override/list/global; `EffectiveMaxTurns`/
|
||||
`EffectiveSystemPromptHint` from list tier; resets clear; `SaveAsync` builds the right
|
||||
`UpdateTaskAgentSettingsDto`.
|
||||
- `InheritanceResolverTests` unchanged (resolver untouched).
|
||||
- Existing DetailsIsland* tests must stay green (they construct the VM but don't name the
|
||||
moved members).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `dotnet build -c Release` clean for Ui (+ App).
|
||||
- `Ui.Tests` + `Localization.Tests` green.
|
||||
- One editor VM + one control drive both List and Task; duplicated field/badge/reset
|
||||
logic deleted; `ResolveList` now has a real caller.
|
||||
- Visual gap flagged: open List Settings → Agent, and a task's gear flyout — verify
|
||||
badges, ↺ resets, reset-all, agent browse (list only), system-prompt hint (task), and
|
||||
that list Save persists + task auto-saves.
|
||||
|
||||
## Commit
|
||||
|
||||
`refactor(agent-config): single AgentConfigEditor for list + task scopes`. Stage by
|
||||
path. Commit this plan with it.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Phase 5 — DiffViewer (A1 + B2)
|
||||
|
||||
Date: 2026-06-23
|
||||
Umbrella: `docs/superpowers/plans/2026-06-19-feature-unification-plan.md`
|
||||
Design: `docs/superpowers/specs/2026-06-19-feature-unification-design.md` (A1, B2)
|
||||
|
||||
## Goal
|
||||
|
||||
One diff component replaces the three parallel read-only diff windows:
|
||||
`DiffModalViewModel`/View, `WorktreeModalViewModel`/View, `PlanningDiffViewModel`/View.
|
||||
**Merge editor (`ConflictResolverViewModel`) is untouched** — per the design's hard
|
||||
decision; the viewer only *opens* it on conflict via the existing Merge flow.
|
||||
|
||||
All three are already master-detail: **left nav pane + right `DiffLinesView`**. They
|
||||
differ only in left-pane content, chrome, and data source — so they collapse into one
|
||||
shell with a source mode.
|
||||
|
||||
## Decisions (Mika, 2026-06-23)
|
||||
|
||||
- **File nav = file-tree** (folder-grouped), not a flat list. Port `WorktreeModal`'s tree
|
||||
+ the Avalonia-12 `TreeView.SelectionChanged` workaround. Carry per-file status + +adds/
|
||||
−dels into the tree rows (from the parsed `DiffFileViewModel`).
|
||||
- Planning keeps its **subtask-list + combined-mode toggle**; the branch source keeps its
|
||||
**Merge** button.
|
||||
|
||||
## Target
|
||||
|
||||
### Shared types → `ViewModels/Modals/DiffModels.cs` (new, same namespace)
|
||||
|
||||
Move out of the to-be-deleted VMs so `UnifiedDiffParser`/`DiffLinesView` keep compiling:
|
||||
`DiffLineKind`, `DiffFileStatus`, `DiffLineViewModel`, `DiffFileViewModel` (from
|
||||
`DiffModalViewModel.cs`), `SubtaskDiffRow` (from `PlanningDiffViewModel.cs`). Add new
|
||||
`DiffTreeNodeViewModel` (dir/file node; file leaves hold their `DiffFileViewModel`).
|
||||
|
||||
### `DiffViewerViewModel` (`ViewModels/Modals/DiffViewerViewModel.cs`, new)
|
||||
|
||||
ctor `(GitService git, IWorkerClient worker)`. A `DiffViewerMode { Files, Planning }`.
|
||||
|
||||
- **File sources** (replaces DiffModal + WorktreeModal): config props `WorktreePath`,
|
||||
`BaseRef`, `HeadCommit`, `FromCommitRange`, `TaskId`, `TaskTitle` + `ShowMergeModal`/
|
||||
`ResolveMergeVm` delegates. `LoadAsync` pulls the whole diff via GitService
|
||||
(`GetCommitRangeDiffAsync` | `GetBranchDiffAsync` | `GetDiffAsync`), parses with
|
||||
`UnifiedDiffParser.Parse`, builds `FileTree`. `SelectedNode` (leaf) → `SelectedFile`
|
||||
(header + binary/empty placeholders + `Lines`). Commit-range null-guard → "no longer
|
||||
available" (preserve DiffModal behavior). `MergeCommand` (CanMerge = TaskId +
|
||||
delegates) opens the MergeModal, closes on merged/routed (verbatim from DiffModal).
|
||||
- **Planning source** (replaces PlanningDiff): config `PlanningTaskId`, `TargetBranch`.
|
||||
`LoadAsync` pulls `GetPlanningAggregateAsync` → `Subtasks`; `SelectedSubtask` →
|
||||
`DisplayedDiff`; `IsCombinedMode` toggle → `BuildPlanningIntegrationBranchAsync`
|
||||
(success → combined diff; conflict → `CombinedWarning` with subtask + file count;
|
||||
null → hub-error warning). `DisplayedDiff` → flattened `DiffLines` (right pane).
|
||||
- Shared: `StatusMessage`, `CloseAction`, `CloseCommand`.
|
||||
|
||||
### `DiffViewerView` (`Views/Modals/DiffViewerView.axaml` + `.cs`, new)
|
||||
|
||||
`ModalShell`-based window. Left pane: `TreeView` (Files mode) or subtask `ListBox`
|
||||
(Planning mode), toggled by mode. Right pane: the DiffModal file pane (header + binary/
|
||||
empty/no-changes placeholders + `DiffLinesView Lines="SelectedFile.Lines"`) in Files mode,
|
||||
or `DiffLinesView Lines="DiffLines"` in Planning mode. Toolbar: combined toggle + warning
|
||||
+ loading (Planning). Footer: Merge button (Files mode, CanMerge). Code-behind: `CloseAction`,
|
||||
the `TreeView.SelectionChanged` → `SelectedNode` workaround, dir-row tap-to-expand.
|
||||
|
||||
### Re-point the 3 doors → one viewer
|
||||
|
||||
- **`MergeSectionViewModel`**: `OpenDiffAsync` builds a Files-mode `DiffViewerViewModel`
|
||||
(+ ShowMergeModal/ResolveMergeVm) and calls a single `ShowDiffViewer` delegate;
|
||||
`ReviewCombinedDiffAsync` builds a Planning-mode one and calls the *same* delegate.
|
||||
Replaces `ShowDiffModal` + `ShowPlanningDiffModal` with one `Func<DiffViewerViewModel,Task>
|
||||
ShowDiffViewer`; keeps `ShowMergeModal`. (Resolve the VM via `_services`.)
|
||||
- **`DetailsIslandView.axaml.cs`**: replace the two `ShowDiffModal`/`ShowPlanningDiffModal`
|
||||
wirings (→ `DiffModalView`/`PlanningDiffView`) with one `ShowDiffViewer` (→ `DiffViewerView`).
|
||||
Keep `ShowMergeModal`.
|
||||
- **`WorktreesOverviewModalViewModel`**: `ShowDiff` builds a Files-mode viewer (worktree path
|
||||
+ base). Change `_diffVmFactory` from `Func<WorktreeModalViewModel>` to
|
||||
`Func<DiffViewerViewModel>`; `ShowDiffAction` stays `Action<DiffViewerViewModel>`.
|
||||
- **`WindowDialogService.cs`**: `ShowDiffAction` → `new DiffViewerView` + `LoadAsync` + show.
|
||||
- **`Program.cs`**: register `DiffViewerViewModel` (transient) + `Func<DiffViewerViewModel>`;
|
||||
drop the `WorktreeModalViewModel` registration.
|
||||
|
||||
### Delete
|
||||
|
||||
`DiffModalViewModel.cs`, `WorktreeModalViewModel.cs`, `PlanningDiffViewModel.cs`,
|
||||
`DiffModalView.axaml(.cs)`, `WorktreeModalView.axaml(.cs)`, `PlanningDiffView.axaml(.cs)`.
|
||||
|
||||
### Localization
|
||||
|
||||
Reuse existing keys in the merged view (`modals.diff.*` for the file pane, `planning.diff.*`
|
||||
for the planning toolbar). Prune clearly-orphaned `modals.worktree.*` if trivial; keep en/de
|
||||
parity.
|
||||
|
||||
## Tests
|
||||
|
||||
Replace `DiffModalViewModelTests` + `PlanningDiffViewModelTests` with
|
||||
`DiffViewerViewModelTests` preserving the behaviors: commit-range null-guard → unavailable;
|
||||
planning init populates + selects first; subtask select → DisplayedDiff; combined toggle
|
||||
success/conflict/null. `WorktreesOverviewBatchMergeTests` compiles unchanged (`() => null!`
|
||||
satisfies the new Func type). `UnifiedDiffParserTests` unchanged.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `dotnet build -c Release` clean (App); `Ui.Tests` + `Localization.Tests` green.
|
||||
- One viewer reached from all 3 doors; old VMs/views deleted; merge editor untouched.
|
||||
- Visual gap flagged: Details "Open Diff" (dirty + post-merge commit-range), Worktrees-
|
||||
Overview "Show Diff" (tree), Details "Review Combined Diff" (subtasks + combined toggle),
|
||||
and the Merge button still opens the merge form / resolver on conflict.
|
||||
|
||||
## Commit
|
||||
|
||||
`refactor(diff): single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff`.
|
||||
Stage by path (exclude concurrent peers' files). Then Phase 3 (WorktreeActions) follows as
|
||||
its own slice, reusing this viewer.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Plan — Worker log → footer + Log Visualizer overlay
|
||||
|
||||
Design: `docs/superpowers/specs/2026-06-23-worker-log-footer-overlay-design.md`. Build on `main`, TDD, commit per task (Conventional Commits, explicit paths — shared worktree). Build `-c Release`.
|
||||
|
||||
## Task 1 — `LogRingBuffer` (Worker) + tests
|
||||
- `src/ClaudeDo.Worker/Logging/WorkerLogRecord.cs` — `record WorkerLogRecord(string Message, WorkerLogLevel Level, DateTime TimestampUtc)`.
|
||||
- `src/ClaudeDo.Worker/Logging/LogRingBuffer.cs` — thread-safe, `TimeSpan window` + int cap; `Append(record)`, `Snapshot()`. Uses an injected clock func (`Func<DateTime>`) for testability (default `() => DateTime.UtcNow`).
|
||||
- Tests: age eviction, cap eviction, snapshot order. **No `DateTime.UtcNow` in tests — drive the clock.**
|
||||
|
||||
## Task 2 — `BroadcastLogSink` (Worker) + tests
|
||||
- `src/ClaudeDo.Worker/Logging/BroadcastLogSink.cs : ILogEventSink` — level map, render (+exception first line), append-all-levels, broadcast Warn/Err via deferred `HubBroadcaster` (`Attach`), dedupe window (const 120s), loop-guard (skip SignalR `SourceContext` for broadcast; swallow broadcast exceptions). Inject clock func.
|
||||
- Broadcaster is an abstraction the test can fake: depend on a tiny `Func<string,WorkerLogLevel,DateTime,Task>?` set by `Attach`, OR on `HubBroadcaster` directly (it's a sealed class — prefer a delegate to keep the test pure). Use a delegate.
|
||||
- Tests: all levels buffered; only Warn/Err invoke the broadcast delegate; dedupe suppresses 2nd identical within window but still buffers; exception rendering; SignalR-source event buffered but not broadcast.
|
||||
|
||||
## Task 3 — wire into `Program.cs` + `WorkerHub.GetRecentLogs`
|
||||
- `Program.cs`: create `LogRingBuffer` + `BroadcastLogSink` locals before build; `.WriteTo.Sink(broadcastSink)`; `AddSingleton(logBuffer)`; after build `broadcastSink.Attach((m,l,t) => broadcaster.WorkerLog(m,l,t))` using resolved `HubBroadcaster`.
|
||||
- `WorkerHub`: inject `LogRingBuffer`; `public IReadOnlyList<WorkerLogRecordDto> GetRecentLogs()` → snapshot mapped to DTO. Add `WorkerLogRecordDto` (Hub or shared). Update `WorkerHub` ctor → check hub-construction call sites/tests.
|
||||
- Build Worker `-c Release`; run Worker.Tests (filtered to new + hub).
|
||||
|
||||
## Task 4 — `IWorkerClient.GetRecentLogsAsync` + WorkerClient + fakes
|
||||
- `IWorkerClient` + `WorkerClient` impl (`_hub.InvokeAsync<List<WorkerLogEntry>>("GetRecentLogs", ct)`).
|
||||
- Update fakes: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, Worker.Tests UiVm fake(s) → return `Array.Empty<WorkerLogEntry>()`.
|
||||
- Build Ui + Worker.Tests.
|
||||
|
||||
## Task 5 — `LogVisualizerViewModel` + View + dialog wiring + tests
|
||||
- VM (Modals/), View (Modals/, ModalShell), `IDialogService.ShowLogVisualizerAsync` + `WindowDialogService` impl.
|
||||
- `IslandsShellViewModel.OpenLogVisualizerCommand` (resolves VM, loads, shows). Make footer worker-log line a clickable Button → command.
|
||||
- Localization `vm.logVisualizer` en+de.
|
||||
- Tests: VM load/populate/filter. Build App `-c Release`; Ui.Tests + Localization.Tests.
|
||||
|
||||
## Task 6 — verify + docs
|
||||
- Full relevant test pass. Update `src/ClaudeDo.Ui/CLAUDE.md` (overlay VM/view, footer click) + `src/ClaudeDo.Worker/CLAUDE.md` (Logging/ folder, sink, GetRecentLogs, WorkerLog now carries Serilog Warn/Err). Note visual-verification gap (overlay render) for the user.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Plan — Interactive "Answer Claude's Questions"
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-25-interactive-ask-user-design.md`
|
||||
|
||||
Implement on the shared main tree. Commit explicit paths per task (never `git add -A`).
|
||||
Build with `-c Release` (running Worker locks Debug). No real-Claude tests.
|
||||
|
||||
## Task 1 — PendingQuestionRegistry (worker, new file)
|
||||
- `src/ClaudeDo.Worker/Runner/PendingQuestionRegistry.cs`: singleton; `record PendingQuestion(TaskId, QuestionId, Question)`.
|
||||
- `(string QuestionId, Task<string> Answer) Register(taskId, question)` — overwrites any stale entry, `RunContinuationsAsynchronously`.
|
||||
- `bool TryAnswer(taskId, questionId, answer)`; `PendingQuestion? Get(taskId)`; `void Remove(taskId, questionId)`.
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/PendingQuestionRegistryTests.cs` — register→answer resolves the task; wrong questionId no-ops; Get reflects state; second Register overwrites.
|
||||
|
||||
## Task 2 — AskUser MCP tool (worker)
|
||||
- `TaskRunMcpService.cs`: inject `PendingQuestionRegistry`; add
|
||||
`[McpServerTool] async Task<string> AskUser(string question, CancellationToken ct)`:
|
||||
- caller id from `_ctx.Current.CallerTaskId`; register; broadcast `TaskQuestionAsked`.
|
||||
- await answer via `Task<string>.WaitAsync` with a 3-min linked-CTS; on timeout return the fallback string; on request-cancel rethrow.
|
||||
- `finally`: `Remove` + broadcast `TaskQuestionResolved`.
|
||||
- `[Description]`: when to use (only when a wrong guess is costly/irreversible; otherwise proceed).
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/AskUserToolTests.cs` — answer path returns the answer; timeout path returns fallback (inject a short timeout or a seam) with a fake broadcaster + stub context accessor.
|
||||
|
||||
## Task 3 — Wire MCP for all runs + timeout env (worker)
|
||||
- `TaskRunner.RunAsync`: move MCP-identity setup out of the `standalone` gate so every run gets `claudedo_run`; `AllowedTools` = `mcp__claudedo_run__AskUser` always, append `,mcp__claudedo_run__SuggestImprovement` when standalone. Keep token cleanup in `finally`.
|
||||
- `ClaudeProcess.cs`: `psi.Environment["MCP_TOOL_TIMEOUT"] = "200000";`.
|
||||
- System prompt file (PromptKind.System default): add one guidance line about `AskUser`.
|
||||
|
||||
## Task 4 — Hub + Broadcaster (worker)
|
||||
- `HubBroadcaster.cs`: `TaskQuestionAsked(taskId, questionId, question)`, `TaskQuestionResolved(taskId, questionId)`.
|
||||
- `WorkerHub.cs`: inject registry; `bool AnswerTaskQuestion(taskId, questionId, answer)`; `PendingQuestionDto? GetPendingQuestion(taskId)`; `record PendingQuestionDto(...)`.
|
||||
- `Program.cs`: register `PendingQuestionRegistry` as singleton.
|
||||
|
||||
## Task 5 — UI client (IWorkerClient/WorkerClient + fakes)
|
||||
- `IWorkerClient`: `Task AnswerTaskQuestionAsync(taskId, questionId, answer)`, `Task<PendingQuestionDto?> GetPendingQuestionAsync(taskId)`, events `Action<string,string,string>? TaskQuestionAskedEvent`, `Action<string,string>? TaskQuestionResolvedEvent`; UI DTO record.
|
||||
- `WorkerClient`: implement invokes + `On<...>` handlers raising the events.
|
||||
- Update hand-rolled `IWorkerClient` fakes in Ui.Tests (and Worker.Tests if present).
|
||||
|
||||
## Task 6 — TaskMonitorViewModel (hot file)
|
||||
- Subscribe both events (filter by `_subscribedTaskId`); dispose handlers.
|
||||
- Props: `PendingQuestionId`, `PendingQuestion`, `HasPendingQuestion`, `AnswerDraft`, `IsWaitingForInput`.
|
||||
- `SubmitAnswerCommand` (CanExecute: non-empty draft + HasPendingQuestion) → `AnswerTaskQuestionAsync`; clear draft.
|
||||
- Clear pending on `TaskFinished` for this task and in `Reset()`.
|
||||
- Test: `TaskMonitorViewModelTests` — asked event surfaces question; submit invokes client + clears; resolved/finished clears.
|
||||
|
||||
## Task 7 — Hydrate on attach (MissionControlViewModel)
|
||||
- In `HydrateAsync`, after `ApplyState`, call `GetPendingQuestionAsync(taskId)`; if present, set the monitor's pending question (re-attach case).
|
||||
|
||||
## Task 8 — View banner (hot file, additive)
|
||||
- `MonitorPaneView.axaml`: a `Border DockPanel.Dock="Top"` above `SessionTerminalView`, `IsVisible="{Binding HasPendingQuestion}"`, showing the question text, a `TextBox` bound to `AnswerDraft` (Enter submits), and a Send `Button` → `SubmitAnswerCommand`. Mirror the roadblock-banner styling.
|
||||
|
||||
## Task 9 — Localization
|
||||
- `en.json` + `de.json`: `missionControl.question.title`, `.placeholder`, `.send`. Keep parity (Localization.Tests).
|
||||
|
||||
## Task 10 — Build + test + verify
|
||||
- `dotnet build` App + Worker `-c Release`; run Worker.Tests, Ui.Tests, Localization.Tests.
|
||||
- Self-review diffs. Flag the two manual verification gaps to Mika. Do not push.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Plan — Mission Control (multi-task live monitoring)
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-25-mission-control-design.md`
|
||||
|
||||
Execution: subagent-driven, **sonnet** model, TDD where a test is meaningful, build + test before
|
||||
each commit, one Conventional Commit per task. Stage files explicitly by path (never `git add -A`).
|
||||
**No duplication** — every task reuses the assets named in the spec's reuse map.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Extract the reusable monitor core (no behavior change)
|
||||
|
||||
### Task 1.1 — Move `LogLineViewModel` + `LogKind` to their own file
|
||||
- Cut `LogKind` enum and `LogLineViewModel` from `DetailsIslandViewModel.cs` into
|
||||
`ViewModels/Islands/LogLineViewModel.cs` (same namespace). No logic change.
|
||||
- Build `ClaudeDo.App`; run Ui.Tests. Commit: `refactor(ui): split LogLineViewModel into own file`.
|
||||
|
||||
### Task 1.2 — Create `TaskMonitorViewModel` owning the streaming/status/outcome core
|
||||
- New `ViewModels/Islands/TaskMonitorViewModel.cs`. Move from `DetailsIslandViewModel`:
|
||||
`Log`, `_subscribedTaskId`, `_formatter`, `_claudeBuf`, `OnTaskMessage`, `AppendStdoutLine`,
|
||||
`FlushClaudeBuffer`, `ReplayLogFileAsync`, `ExpandUserPath`; `AgentState` + all `Is*` flags +
|
||||
`OnAgentStateChanged`; `StatusToStateKey` / `FinishedStatusToStateKey`; `SessionOutcome` /
|
||||
`Roadblocks` + `ApplyOutcome` + `RoadblockMarker`; the worker `TaskMessage/Started/Finished/Updated`
|
||||
subscriptions for the streaming concern; `Title`/`TaskIdBadge`/`Model`/`TurnsText`/`TokensFormatted`/
|
||||
diff text/elapsed; `BlockingReason` (+visible flag) from `BlockedByTaskId`/review/children/roadblocks.
|
||||
- Ctor takes `IDbContextFactory<ClaudeDoDbContext>`, `IWorkerClient`. `Attach(taskId)` /
|
||||
`AttachAsync(entity)` to (re)bind + replay; `IDisposable` unsubscribes (mirror existing Dispose).
|
||||
- Unit test (Ui.Tests): feed `[stdout]`/`[claude]`/`[tool]` lines via the worker fake → `Log`
|
||||
accumulates correctly; `TaskFinished` flips `AgentState`; `ApplyOutcome` splits the roadblock marker.
|
||||
Reuse the existing IWorkerClient fake (see `iworkerclient_fakes_sync`).
|
||||
- Build + test. Commit: `feat(ui): extract TaskMonitorViewModel streaming core`.
|
||||
|
||||
### Task 1.3 — `DetailsIslandViewModel` delegates to `Monitor`
|
||||
- Add `public TaskMonitorViewModel Monitor { get; }`; construct it; route `Bind`/`BindAsync` to
|
||||
`Monitor.Attach`. Remove the moved members; keep subtasks/attachments/editing/merge/review/child
|
||||
outcomes/notes/prep intact. Dispose `Monitor`.
|
||||
- Repoint `WorkConsole.axaml` Output-tab bindings (`Log`, `IsRunning/IsDone/IsFailed`,
|
||||
`SessionOutcome`, `TurnsText`, `DiffAddText`/`DiffDelText`, `Model`) to `Monitor.*`. Leave
|
||||
review/merge/session bindings unchanged.
|
||||
- Build + test. **Manual visual pass: Details pane behaves exactly as before** (flag for Mika).
|
||||
Commit: `refactor(ui): route DetailsIsland streaming through Monitor`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Mission Control window
|
||||
|
||||
### Task 2.1 — `MissionControlViewModel`
|
||||
- New `ViewModels/MissionControlViewModel.cs`: `ObservableCollection<TaskMonitorViewModel> Monitors`
|
||||
keyed by id; seed from `GetActive()`; add on `TaskStarted`, flip-state-and-keep on `TaskFinished`;
|
||||
`ClearFinished` command; `ColumnCount`/layout signal from `Monitors.Count`; least-active collapse.
|
||||
`IDisposable` disposes all monitors. Inject `IDbContextFactory`, `IWorkerClient`, `IServiceProvider`.
|
||||
- Register `AddSingleton<MissionControlViewModel>` in `App/Program.cs`.
|
||||
- Unit test: simulate two `TaskStarted` → two monitors; `TaskFinished` keeps the pane; `ColumnCount`
|
||||
matches count. Commit: `feat(ui): add MissionControlViewModel`.
|
||||
|
||||
### Task 2.2 — `RevealTaskAsync` navigation on the shell
|
||||
- Add `IslandsShellViewModel.RevealTaskAsync(taskId)` (resolve list → select → await load → select row).
|
||||
- Wire `TaskMonitorViewModel.OpenInApp` to it (via an `Action<string>?` set by the shell, like the
|
||||
existing `CloseDetail`/`DeleteFromList` hooks — no new DI cycle).
|
||||
- Unit test for the select-by-id path. Commit: `feat(ui): reveal a task by id from anywhere`.
|
||||
|
||||
### Task 2.3 — `MonitorPaneView` (reuses `SessionTerminalView`)
|
||||
- New `Views/MissionControl/MonitorPaneView.axaml(.cs)`: header (title/chip/tok/turn/elapsed),
|
||||
blocking banner (`live-chip`/`terminal`/error-tint classes from IslandStyles — reuse), body =
|
||||
`<SessionTerminalView Entries="{Binding Log}" ... />`, footer (Open in app / Detach / Cancel).
|
||||
`x:DataType=TaskMonitorViewModel`. No new console control. Add `missionControl.*` en+de keys.
|
||||
- Build + Localization.Tests. Commit: `feat(ui): add MonitorPaneView`.
|
||||
|
||||
### Task 2.4 — `MissionControlView` grid + `MissionControlWindow`
|
||||
- `MissionControlView.axaml`: `ItemsControl`/`UniformGrid` of `MonitorPaneView` driven by `ColumnCount`,
|
||||
horizontal scroll fallback, header with `ClearFinished` (+ optional QuickAdd, deferrable).
|
||||
- `MissionControlWindow.axaml(.cs)`: hosts the view; lazy-create + hide-on-close.
|
||||
- Build. Commit: `feat(ui): add MissionControl window + grid`.
|
||||
|
||||
### Task 2.5 — Launch button + lifetime
|
||||
- Title-bar toggle button in `MainWindow.axaml` → shell command that shows/focuses the window
|
||||
(created lazily, owns the singleton VM).
|
||||
- Set `desktop.ShutdownMode = OnMainWindowClose` in `App.OnFrameworkInitializationCompleted`.
|
||||
- Build. **Manual visual pass** (flag for Mika): open with 2+ running tasks; main window still adds
|
||||
tasks; blocking banner; Open-in-app. Commit: `feat(ui): open Mission Control from the title bar`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Per-pane detach (lowest priority)
|
||||
|
||||
### Task 3.1 — `TaskMonitorWindow` + detach/re-dock
|
||||
- `Views/MissionControl/TaskMonitorWindow.axaml(.cs)` hosting `MonitorPaneView`; `Detach` removes the
|
||||
monitor from the grid and shows it in the window (optional always-on-top); close re-docks.
|
||||
- Build. Manual visual pass. Commit: `feat(ui): detach a monitor into its own window`.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting checklist (every task)
|
||||
- Stage by explicit path; sonnet subagents; reuse per the spec's map — no new console/streaming/insert path.
|
||||
- en.json + de.json parity for any new string (Localization.Tests).
|
||||
- If `IWorkerClient`/ctor signatures change, update the hand-rolled fakes in **both** test projects.
|
||||
- Build `ClaudeDo.App` (`-c Release` if Worker is running) before marking a task done.
|
||||
- Never push without asking.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Plan — In-App Interactive Sessions
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-06-26-in-app-interactive-sessions-design.md`
|
||||
|
||||
Implement on the shared main tree. Commit explicit paths per task (never `git add -A`).
|
||||
Build with `-c Release` (running Worker locks Debug). No real-Claude tests — fake the
|
||||
process stream. Sonnet subagents. Autonomous `TaskRunner`/`ClaudeProcess` path stays untouched.
|
||||
|
||||
## Task 1 — StreamingClaudeSession (worker, new file)
|
||||
- `Runner/StreamingClaudeSession.cs`: persistent `claude` process. Ctor takes resolved args,
|
||||
working dir, seeded first prompt, a line callback, `WorkerConfig`. Reuse the
|
||||
`ProcessStartInfo` shape + `MCP_TOOL_TIMEOUT="200000"` from `ClaudeProcess`.
|
||||
- Keeps stdin open; sends the first prompt as a user-message JSON line (escape via
|
||||
`JsonSerializer`).
|
||||
- stdout/stderr read tasks → line callback; parse `result` events to track `IsTurnInFlight`.
|
||||
- `SendUserMessageAsync(text, ct)` — enqueue/write a user-message JSON line; if
|
||||
`IsTurnInFlight`, also `InterruptAsync`.
|
||||
- `InterruptAsync(ct)` — write the control-protocol interrupt line; best-effort (swallow +
|
||||
log on failure → queue fallback applies).
|
||||
- `StopAsync` / `DisposeAsync` — close stdin, kill the tree, await exit.
|
||||
- Injectable stream seam so a fake can drive it without a real `claude` binary.
|
||||
- Test: `StreamingClaudeSessionTests` (fake stream) — first message emitted; `result` flips
|
||||
`IsTurnInFlight` off; a sent message produces a second turn; mid-turn send calls interrupt
|
||||
then delivers; interrupt throw → delivered at natural turn end; stop kills.
|
||||
|
||||
## Task 2 — LiveSessionRegistry (worker, new file)
|
||||
- `Runner/LiveSessionRegistry.cs`: singleton; `Register(taskId, StreamingClaudeSession)`,
|
||||
`bool TryGet(taskId, out session)`, `Unregister(taskId)`, `Task StopAsync(taskId)`.
|
||||
- Test: register→get; unregister; second register stops+replaces; missing get returns false.
|
||||
|
||||
## Task 3 — InteractiveSessionService (worker, new file)
|
||||
- `Planning/InteractiveSessionService.cs`: inject `IDbContextFactory`, `WorkerConfig`,
|
||||
`ClaudeArgsBuilder` (or build args inline), `HubBroadcaster`, `LiveSessionRegistry`.
|
||||
- `StartAsync(taskId, ct)`: resolve list working dir + seeded prompt (reuse the body of
|
||||
`PlanningSessionManager.OpenInteractiveAsync` + `BuildInteractivePrompt`); build interactive
|
||||
args (`--model PlanningAlias --permission-mode auto` + streaming flags); spawn the session
|
||||
with a callback that does `HubBroadcaster.TaskMessage(taskId, "[stdout] " + line)`;
|
||||
register; broadcast `InteractiveSessionStarted`. Reject if one is already live for the task.
|
||||
- `SendAsync(taskId, text, ct)` → registry `TryGet` → `SendUserMessageAsync`.
|
||||
- `StopAsync(taskId, ct)` → registry stop + `InteractiveSessionEnded`.
|
||||
- Move `OpenInteractiveAsync`/`BuildInteractivePrompt` out of `PlanningSessionManager` if it
|
||||
reads cleaner (or call into it). Remove the `InteractiveLaunchContext` terminal coupling.
|
||||
- Test: `InteractiveSessionServiceTests` (fake session factory + fake broadcaster) — start
|
||||
resolves dir, seeds prompt, registers, broadcasts started; missing working dir throws;
|
||||
send routes; stop broadcasts ended.
|
||||
|
||||
## Task 4 — Remove terminal interactive path (worker)
|
||||
- `Planning/Interfaces/ITerminalLauncher.cs` + `WindowsTerminalLauncher.cs`: delete
|
||||
`LaunchInteractiveAsync`; remove `InteractiveLaunchContext` from `PlanningSessionContext.cs`.
|
||||
Keep planning start/resume launches.
|
||||
- Fix any references; ensure the planning launcher tests still build.
|
||||
|
||||
## Task 5 — Hub + Broadcaster + DI (worker)
|
||||
- `Hub/WorkerHub.cs`: re-point `OpenInteractiveTerminalAsync` to
|
||||
`InteractiveSessionService.StartAsync` (drop `_launcher.LaunchInteractiveAsync`); add
|
||||
`Task SendInteractiveMessage(taskId, text)`, `Task StopInteractiveSession(taskId)`
|
||||
(+ optional `InterruptInteractiveSession`).
|
||||
- `Hub/HubBroadcaster.cs`: `InteractiveSessionStarted(taskId)`, `InteractiveSessionEnded(taskId)`.
|
||||
- `Program.cs`: register `LiveSessionRegistry` + `InteractiveSessionService` singletons.
|
||||
- Test: `WorkerHub` send routes to a fake service; start invokes the service.
|
||||
|
||||
## Task 6 — UI client + fakes (ui)
|
||||
- `Services/Interfaces/IWorkerClient.cs` + `WorkerClient.cs`: `SendInteractiveMessageAsync(
|
||||
taskId, text)`, `StopInteractiveSessionAsync(taskId)` (+ optional interrupt); events
|
||||
`Action<string>? InteractiveSessionStartedEvent`, `InteractiveSessionEndedEvent` with
|
||||
`On<...>` handlers. `OpenInteractiveTerminalAsync` keeps name/signature.
|
||||
- Update hand-rolled `IWorkerClient` fakes in **both** Ui.Tests and Worker.Tests.
|
||||
|
||||
## Task 7 — StreamLineFormatter user bubble (ui)
|
||||
- Render `type:"user"` NDJSON events as `LogKind.User` (add the kind if missing).
|
||||
- Test: a `user` event yields a `LogKind.User` `LogLineViewModel` with the text.
|
||||
|
||||
## Task 8 — Shared composer state on the session VMs (ui, hot files)
|
||||
- Add to `TaskMonitorViewModel` and `DetailsIslandViewModel` (factor a shared helper —
|
||||
`InteractiveComposer` — to avoid duplication): `ComposerDraft`, `IsInteractiveLive`
|
||||
(toggled by `InteractiveSessionStarted/Ended` for the subscribed task),
|
||||
`SubmitComposerCommand` (CanExecute: non-empty draft && (`HasPendingQuestion` ||
|
||||
`IsInteractiveLive`)). Route: pending question → existing `AnswerTaskQuestionAsync`; else →
|
||||
`SendInteractiveMessageAsync`. Clear draft on submit; clear `IsInteractiveLive` on ended.
|
||||
- `MissionControlViewModel`: `EnsureMonitor(taskId)` on `InteractiveSessionStarted`.
|
||||
- Test: composer enabled while interactive-live; submit routes (chat vs answer) + clears;
|
||||
ended clears live state.
|
||||
|
||||
## Task 9 — SessionTerminalView composer (ui)
|
||||
- `Views/Islands/SessionTerminalView.axaml(.cs)`: optional composer docked bottom (styled
|
||||
props `IsComposerVisible`, `ComposerText`, `SubmitCommand`, `ComposerPlaceholder`); TextBox
|
||||
(Enter submits) + Send button. Reuse existing tokens (no inline values).
|
||||
- Bind it in `MonitorPaneView.axaml` and `DetailsIslandView.axaml` to each VM's composer
|
||||
state. Fold the existing AskUser banner into the composer's "answering" state if it reads
|
||||
cleaner; otherwise leave the banner and add the composer below.
|
||||
|
||||
## Task 10 — Localization
|
||||
- `en.json` + `de.json`: `interactive.composer.placeholder`, `.send`, `.stop`, plus any
|
||||
"session ended" notice. Keep parity (Localization.Tests).
|
||||
|
||||
## Task 11 — Build + test + verify
|
||||
- Build App + Worker `-c Release`; run Worker.Tests, Ui.Tests, Localization.Tests.
|
||||
- Self-review diffs. **Manual smoke (real CLI) — flag to Mika:** (a) Run interactively opens
|
||||
an in-app chat (no terminal) and streams; (b) sending a message mid-turn interrupts +
|
||||
redirects; (c) stop kills the process; (d) session shows in both task detail and Mission
|
||||
Control. Do not push.
|
||||
@@ -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,80 @@
|
||||
# Per-task model override via MCP + cheapest-model prompt guidance
|
||||
|
||||
Date: 2026-06-09
|
||||
|
||||
## Goal
|
||||
|
||||
Let Claude pick the model for each task it generates (planning subtasks,
|
||||
improvement follow-ups, external task creation) directly at creation time via
|
||||
MCP, and instruct Claude — in the relevant prompts — to choose the *cheapest*
|
||||
model that can do the job well.
|
||||
|
||||
## Background
|
||||
|
||||
- `TaskEntity.Model` (nullable) already exists and is resolved
|
||||
task → list-config → global default in `TaskRunner.ResolveConfigAsync`, then
|
||||
passed to the CLI as `--model` by `ClaudeArgsBuilder`.
|
||||
- Today the model can only be set *after* creation via `set_task_config`
|
||||
(`ConfigMcpTools.SetTaskConfig`). The creation tools (`CreateChildTask`,
|
||||
`SuggestImprovement`, `AddTask`) accept no model, so assigning one is a
|
||||
two-call dance.
|
||||
- `ModelRegistry.Aliases = ["sonnet","opus","haiku"]`; no cost ordering or
|
||||
validation helper exists.
|
||||
|
||||
No schema change is required — only plumbing a `model` argument through the
|
||||
creation paths plus prompt edits.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Validation:** strict alias-only. `model` must be one of haiku/sonnet/opus
|
||||
(case-insensitive); blank/null means "inherit" (no override); anything else
|
||||
throws an MCP error so Claude self-corrects immediately rather than the task
|
||||
failing later at CLI runtime.
|
||||
- **`AddSubtask` is out of scope:** it creates a `SubtaskEntity` (a checklist
|
||||
step), which is never independently executed — a model there is a no-op.
|
||||
- **Improvement-child prompt:** the child's model is fixed at filing time and
|
||||
it cannot re-pick, so only a one-line "this is an intentionally small/cheap
|
||||
unit — stay minimal" reminder is added. The real model-choice instruction
|
||||
lives in the main system prompt's SuggestImprovement guidance.
|
||||
|
||||
## Cost ordering & heuristic (single source: `ModelRegistry.ByCostAscending`)
|
||||
|
||||
`haiku < sonnet < opus`
|
||||
|
||||
- **haiku** — trivial/mechanical: doc tweaks, simple renames, small localized edits.
|
||||
- **sonnet** — normal coding work (default).
|
||||
- **opus** — complex architecture, cross-cutting changes, hard debugging.
|
||||
|
||||
## Changes
|
||||
|
||||
1. **`ClaudeDo.Data/Models/ModelRegistry.cs`**
|
||||
- `ByCostAscending = ["haiku","sonnet","opus"]`.
|
||||
- `string? NormalizeAlias(string? model)` — trim; null/blank → null;
|
||||
case-insensitive match → canonical lowercase alias; else throw
|
||||
`ArgumentException` with the allowed list.
|
||||
|
||||
2. **`TaskRepository.CreateChildAsync`** — add optional `string? model = null`;
|
||||
set `child.Model = ModelRegistry.NormalizeAlias(model)`. Single choke-point
|
||||
for both child-creation MCP tools.
|
||||
|
||||
3. **MCP creation tools** (add `model` param, document in `[Description]`):
|
||||
- `PlanningMcpService.CreateChildTask` → forward to `CreateChildAsync`.
|
||||
- `TaskRunMcpService.SuggestImprovement` → forward to `CreateChildAsync`.
|
||||
- `ExternalMcpService.AddTask` → `NormalizeAlias` then set `entity.Model`.
|
||||
|
||||
4. **Prompts (`PromptFiles.cs`)**
|
||||
- `PlanningSystemDefault` — instruct the planner to pass each
|
||||
`CreateChildTask` the cheapest capable model (with the ordering/heuristic).
|
||||
- `SystemDefault` (Out-of-scope improvements) — when filing via
|
||||
`SuggestImprovement`, pass the cheapest capable `model`.
|
||||
- `ImprovementChildDefault` — one-line minimality reminder.
|
||||
|
||||
5. **Tests** (no real CLI):
|
||||
- `NormalizeAlias`: valid aliases (any case), blank/null → null, unknown → throws.
|
||||
- `CreateChildTask` / `SuggestImprovement` / `AddTask` persist the model;
|
||||
invalid model is rejected.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- No DB migration. No locale changes (prompts and MCP descriptions are not
|
||||
localized). No UI changes (existing per-task model display already covers it).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Online Inbox — desktop-side design
|
||||
|
||||
Date: 2026-06-10
|
||||
Status: approved, implementing
|
||||
Related: `docs/online-inbox-api-contract.md` (the API both ends share)
|
||||
|
||||
## Goal
|
||||
|
||||
Let the owner add task ideas and view their Idle backlog from a phone/browser. The desktop
|
||||
ClaudeDo opts in to an online service, syncs its list catalog + Idle backlog up, and pulls
|
||||
web-created tasks down as local `Idle` tasks. Execution stays 100% local.
|
||||
|
||||
This spec covers only the **desktop side** (this repo). The API + web client are built
|
||||
VPS-side against the shared contract.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No remote execution; the Worker still runs everything locally.
|
||||
- No syncing of any task state other than the `Idle` mirror.
|
||||
- No multi-user. Single Zitadel user = the owner.
|
||||
- Web client is create + read only.
|
||||
|
||||
## Opt-in & where things live
|
||||
|
||||
- **Off by default.** When disabled: zero network, zero auth — byte-for-byte today's
|
||||
behaviour. Auth only matters once enabled.
|
||||
- Sync runs in the **Worker** (it owns the DB and already hosts `BackgroundService`s). The
|
||||
opt-in config and the stored refresh token live in `worker.config.json`-adjacent state.
|
||||
- Interactive Zitadel login happens in the **UI** (browser flow), which hands the resulting
|
||||
refresh token to the Worker over SignalR; the Worker persists it (DPAPI) and uses it for
|
||||
headless token refresh during polling.
|
||||
|
||||
## Config (`WorkerConfig`, new `online_inbox` section)
|
||||
|
||||
```jsonc
|
||||
"online_inbox": {
|
||||
"enabled": false,
|
||||
"api_base_url": "", // e.g. https://inbox.claudedo.kuns.dev
|
||||
"poll_interval_seconds": 60,
|
||||
"zitadel": {
|
||||
"authority": "", // issuer URL (from VPS report)
|
||||
"client_id": "",
|
||||
"scopes": "openid offline_access" // offline_access → refresh token
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The refresh token is NOT stored in this file. It lives encrypted via
|
||||
`System.Security.Cryptography.ProtectedData` (DPAPI, CurrentUser) at
|
||||
`~/.todo-app/online-inbox.token` and is read/written only by the Worker.
|
||||
|
||||
## Components (Worker, new `Online/` folder)
|
||||
|
||||
```
|
||||
Worker/Online/
|
||||
OnlineInboxConfig.cs — the config record (bound from WorkerConfig.OnlineInbox)
|
||||
Dtos.cs — RemoteList, RemoteTask, MirrorTask DTOs (match the contract)
|
||||
IOnlineInboxApi.cs — typed client surface (one method per endpoint)
|
||||
OnlineInboxApiClient.cs — HttpClient impl; attaches bearer via IOnlineAuthProvider
|
||||
Interfaces/IOnlineAuthProvider.cs — Task<string?> GetAccessTokenAsync(ct)
|
||||
ZitadelAuthProvider.cs — concrete (PENDING: needs the Zitadel package + client config)
|
||||
OnlineTokenStore.cs — DPAPI-backed refresh-token persistence
|
||||
OnlineSyncService.cs — BackgroundService: the reconcile loop (§contract 5)
|
||||
OnlineBacklog.cs — static helper: the Idle-backlog query/filter (§contract 2)
|
||||
```
|
||||
|
||||
### `IOnlineInboxApi`
|
||||
```
|
||||
Task PutListsAsync(IReadOnlyList<RemoteList> lists, ct)
|
||||
Task<IReadOnlyList<RemoteTask>> GetUnimportedTasksAsync(ct) // GET /tasks?imported=false
|
||||
Task MarkImportedAsync(string id, ct) // POST /tasks/{id}/imported
|
||||
Task PutMirrorAsync(IReadOnlyList<MirrorTask> tasks, ct) // PUT /tasks/mirror
|
||||
```
|
||||
(The desktop never calls `POST /tasks`, `GET /lists`, or `GET /lists/{id}/tasks` — those are
|
||||
web-only.)
|
||||
|
||||
### `IOnlineAuthProvider`
|
||||
Single method `Task<string?> GetAccessTokenAsync(CancellationToken)` returning a bearer token
|
||||
(refreshing transparently), or `null` if not logged in / refresh failed. Abstracting it lets
|
||||
us:
|
||||
- ship and test the sync engine now with a fake provider,
|
||||
- wire the real `ZitadelAuthProvider` once the VPS reports authority/client-id and we add the
|
||||
Zitadel package reference.
|
||||
|
||||
`ZitadelAuthProvider` reads the refresh token from `OnlineTokenStore`, exchanges it for an
|
||||
access token, caches the access token until near expiry. **Marked with a
|
||||
`// TODO(online-inbox)` until the flow is wired.**
|
||||
|
||||
> **Auth correction (2026-06-10):** the `KunsZitadel` nuget package is a *server-side*
|
||||
> resource-server helper (`AddKunsZitadel` → `JwtBearer` token *validation*). It belongs on
|
||||
> the VPS API, NOT the desktop. The desktop must *acquire* tokens, so `ZitadelAuthProvider`
|
||||
> uses a client OIDC flow — `IdentityModel.OidcClient` (auth-code + PKCE, loopback redirect)
|
||||
> or the device-authorization grant — against Zitadel's OIDC endpoints, then persists the
|
||||
> refresh token via `OnlineTokenStore`.
|
||||
|
||||
### `OnlineSyncService` (the loop)
|
||||
- Hosted only when `online_inbox.enabled == true` (guarded at registration).
|
||||
- Every `poll_interval_seconds`: create a DI scope, resolve `TaskRepository` + `ListRepository`
|
||||
(same pattern as the External MCP app), run the §5 reconcile loop.
|
||||
- Skips a cycle (logs at debug) if `GetAccessTokenAsync` returns null (not logged in).
|
||||
- All failures are caught per-cycle and logged; never crashes the Worker. Network errors back
|
||||
off to the next interval.
|
||||
- Import safety: a pulled task whose `listId` has no local list is skipped + logged (not
|
||||
imported), and NOT marked imported, so it retries once the list exists. Imported tasks land
|
||||
as `Status=Idle, CreatedBy="online"` — they never auto-run; the user queues them locally.
|
||||
|
||||
## UI (later increment, after VPS report)
|
||||
|
||||
- Settings modal → new "Online Inbox" section: enable toggle, API base URL, **Sign in /
|
||||
Sign out** (Zitadel browser/device flow via the OIDC client lib), connection status.
|
||||
- Login produces a refresh token; UI sends it to the Worker via a new hub method
|
||||
`SetOnlineInboxAuth(refreshToken)` → Worker writes it through `OnlineTokenStore`.
|
||||
- Config read/write via hub methods `GetOnlineInboxConfig` / `SetOnlineInboxConfig`
|
||||
(mirrors the existing `GetAppSettings`/`UpdateAppSettings` pattern).
|
||||
- Visual verification is a manual step (flagged — never claimed working without a run).
|
||||
|
||||
## Security
|
||||
|
||||
- Disabled → no network, no token read.
|
||||
- Bearer attached only over HTTPS `api_base_url`; refuse `http://` non-loopback base URLs.
|
||||
- Refresh token encrypted at rest (DPAPI CurrentUser). Never logged.
|
||||
- Imported tasks are `Idle` only — no auto-execution path from the web.
|
||||
|
||||
## Testing
|
||||
|
||||
- `OnlineSyncService` reconcile logic tested against a **fake `IOnlineInboxApi`** + real
|
||||
SQLite (Worker.Tests style): pull→import→flag, mirror set = Idle backlog, list catalog push,
|
||||
unknown-list skip, disabled = no calls, not-logged-in = skipped cycle.
|
||||
- `OnlineBacklog` filter tested directly (excludes children/planning/blocked/non-Idle).
|
||||
- **No real network and no real Zitadel** in tests — fake the api + auth provider. (Consistent
|
||||
with the no-real-Claude-in-tests rule.)
|
||||
- DPAPI token store: round-trip test is Windows-only; guard or keep as a thin wrapper.
|
||||
|
||||
## Open items (need the VPS report)
|
||||
|
||||
- Exact Zitadel authority/issuer, client id, scopes, and **which grant the Zitadel app is
|
||||
registered for** (auth-code+PKCE with which loopback redirect URI, or device-code). This
|
||||
drives the desktop OIDC client implementation.
|
||||
- Final API base URL.
|
||||
- Desktop client OIDC library decision: `IdentityModel.OidcClient` (recommended) vs
|
||||
hand-rolled device-code. (`KunsZitadel` is server-side only — see the auth correction
|
||||
above; it's for the VPS API.)
|
||||
@@ -0,0 +1,91 @@
|
||||
# Feature unification — one component per feature
|
||||
|
||||
Date: 2026-06-19
|
||||
|
||||
## Goal
|
||||
|
||||
ClaudeDo grew organically; several features now exist as parallel implementations
|
||||
or are reachable through many hand-wired entry points. This design maps the
|
||||
duplication and defines a target where **each feature is one component**, reached
|
||||
through one path, with dead code removed.
|
||||
|
||||
## Method
|
||||
|
||||
Mapped via five parallel exploration agents (merge/conflict, review→merge,
|
||||
diff+worktree, task create/edit, UI entry-point inventory), then verified the
|
||||
load-bearing claims by grep/read before writing this. Every file:line below was
|
||||
confirmed against the working tree on 2026-06-19.
|
||||
|
||||
## Key finding: it is NOT three merge engines
|
||||
|
||||
There is **one** merge engine (`TaskMergeService`), wrapped **once** for multi-child
|
||||
units (`PlanningMergeOrchestrator`), with **one** conflict resolver (the Rider
|
||||
3-pane). `Worker/CLAUDE.md` already records "there is no separate 'Merge all' entry —
|
||||
approve is the single review+merge action." What *looks* like 2–3 merge features is
|
||||
**entry-point sprawl** in the UI plus **one dead hunks-API** left over from the
|
||||
Layer-C rework. So unification is mostly UI plumbing + deletion, not re-architecting
|
||||
the engine.
|
||||
|
||||
## Findings — three buckets
|
||||
|
||||
### Bucket A — genuine duplication (parallel implementations of one job)
|
||||
|
||||
| # | Feature | Duplicated components | Shared already |
|
||||
|---|---|---|---|
|
||||
| A1 | Diff viewing | `DiffModalViewModel` (worktree + commit-range), `WorktreeModalViewModel` (file-tree + per-file), `PlanningDiffViewModel` (per-subtask + integration) | `UnifiedDiffParser`, `DiffLinesView` (good) |
|
||||
| A2 | Agent-config editing | `ListSettingsModalViewModel` (list scope), `AgentSettingsSectionViewModel` (task scope); global lives in `SettingsModalViewModel` | `InheritanceResolver`, `InheritedBadge` (good) |
|
||||
| A3 | Worktree actions | `WorktreesOverviewModalViewModel` per-row cmds (Merge/Discard/Keep/ForceRemove/ShowDiff/Jump) vs `MergeSectionViewModel` (Merge/OpenDiff) | same `IWorkerClient` calls |
|
||||
| A4 | Merge display | `AgentStripView` re-displays `MergeSectionViewModel` state | — |
|
||||
|
||||
### Bucket B — entry-point sprawl (one backend, many hand-wired doors)
|
||||
|
||||
| # | Feature | Doors | Evidence |
|
||||
|---|---|---|---|
|
||||
| B1 | Conflict-resolution seam | 5 copies of `Func<string,string,Task>? RequestConflictResolution` | `WorktreesOverviewModalViewModel.cs:83`, `DiffModalViewModel.cs:75`, `MergeModalViewModel.cs:33`, `MergeSectionViewModel.cs:51`, `DetailsIslandViewModel.cs:347` (delegates). Threaded through `MainWindow.axaml.cs:81`, `IslandsShellViewModel.cs:49/202`, `DiffModalViewModel.cs:103`, `MergeSectionViewModel.cs:159` |
|
||||
| B2 | Diff (open) | 3–4 | MergeSection "Open Diff", TaskHeaderBar "Review Merged Diff", WorktreesOverview "Show Diff", Planning "Review Combined" |
|
||||
| B3 | List Settings dialog | 3 | Lists context menu, Tasks header button, shell bridge `IslandsShellViewModel.cs:190-194` |
|
||||
| B4 | Worktrees Overview | 2–3 | Repos menu (global), Lists context menu (per-list) |
|
||||
| B5 | Repo Import | 2 | Repos menu, Lists footer button |
|
||||
|
||||
The conflict-resolution *target* is already single-point (`IslandsShellViewModel.RequestConflictResolutionAsync`, line 49). What is duplicated is the **seam plumbing**: five VMs each own the Func and it is threaded by hand.
|
||||
|
||||
### Bucket C — dead / leftover
|
||||
|
||||
| # | Item | Evidence |
|
||||
|---|---|---|
|
||||
| C1 | Dead hunks conflict API | `TaskMergeService.GetConflictsAsync` (`Lifecycle/TaskMergeService.cs:250`) ← `WorkerHub.GetMergeConflicts` (`Hub/WorkerHub.cs:378`) ← `WorkerClient` `"GetMergeConflicts"` (`Services/WorkerClient.cs:276`) ← `IWorkerClient`. Live resolver uses `GetMergeConflictDocuments` (`WorkerHub.cs:389`). Only `TaskMergeServiceTests.cs:672` still references the old one. |
|
||||
| C2 | Two task-creation paths | UI quick-add `TasksIslandViewModel.AddAsync` writes EF directly (`db.Tasks.Add`); MCP `ExternalMcpService.AddTask` is the service path. They can drift. |
|
||||
| C3 | Stale worktrees | `.claude/worktrees/feat+planning-sessions-ui/…` carries old copies of `DiffModalViewModel`/`ListSettingsModalViewModel`/`WorktreeModalViewModel`; layer-c resolver leftovers. Worktree hygiene, not main code. |
|
||||
| C4 | Naming drift (deferred) | Hub `StartConflictMerge`/`ContinueConflictMerge`/`AbortConflictMerge` (`WorkerHub.cs:367/405/414`) vs service `MergeAsync`/`ContinueMergeAsync`/`AbortMergeAsync`. **Documented as intentional** at `Worker/CLAUDE.md:153`. |
|
||||
|
||||
## Targets — one component per feature
|
||||
|
||||
1. **MergeCoordinator (B1).** Replace the five `RequestConflictResolution` Func seams with one injected coordinator exposing `MergeAsync(taskId, targetBranch)` that owns the "merge → on-conflict open resolver" sequence. Every door (review Approve, Diff Merge button, WorktreesOverview single + batch, Details merge section) calls it. The single resolution point (`IslandsShellViewModel.RequestConflictResolutionAsync`) becomes the coordinator's body.
|
||||
2. **DiffViewer (A1 + B2).** One `DiffViewerViewModel` + view with a `DiffSource` abstraction (`DirtyWorktree | BranchVsBase | CommitRange | PlanningAggregate | IntegrationBranch`) and an optional file-tree pane. Replaces `DiffModal` + `WorktreeModal` + `PlanningDiff` shells; keeps `UnifiedDiffParser`/`DiffLinesView`. All B2 doors open it with a different source.
|
||||
3. **WorktreeActions (A3).** One `WorktreeActionsViewModel` for a single task's worktree (merge/diff/discard/keep/force-remove), reused by both the overview rows and the Details merge section instead of each owning copies.
|
||||
4. **AgentConfigEditor (A2).** One editor component parameterized by scope (`Global | List | Task`) over `InheritanceResolver`, embedded in Settings, List Settings, and the Details panel. Collapses the duplicated property set + reset commands + badges.
|
||||
5. **DialogService (B3–B5).** Consolidate the per-modal `Show*` Func seams (`IslandsShellViewModel.cs:59-71`) into one `IDialogService` with typed open methods (`OpenListSettings(list)`, `OpenRepoImport()`, `OpenWorktreesOverview(listId?)`…). Menu, context menu, and footer all call the same method; duplicate command definitions across `ListsIsland`/shell collapse to one.
|
||||
6. **Single task-creation path (C2).** Route UI quick-add through the same creation path MCP `AddTask` uses (repository/service), so both honor the same invariants.
|
||||
|
||||
Plus **C1** (delete dead hunks API + its test) and **C3** (prune stale worktrees) as groundwork. **C4** naming alignment is **deferred** — it is documented-intentional and would churn the hub + `WorkerClient` + every `IWorkerClient` fake (see the "fakes to sync" hazard) for cosmetic gain.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Phased, each phase ships green.** Six independently buildable/committable slices; cheapest and lowest-risk first (see the plan). No big-bang.
|
||||
- **One plan file per slice.** Matching the 2026-06-05 layer-A/B/C convention, each slice gets its own `docs/superpowers/plans/2026-06-19-unify-<slice>.md` authored when it is picked up. This umbrella plan sequences them and details Phase 0–1.
|
||||
- **DiffViewer (A1) is last.** Highest effort and most UX-sensitive (file-tree vs whole-unified are different layouts); deferring it lets the cheaper wins land first and de-risks the big one.
|
||||
- **Keep the merge engine and the resolver seam contract.** `TaskMergeService`, `PlanningMergeOrchestrator`, `ConflictResolverViewModel` ctor/`OpenAsync`/`OpenForPlanningAsync`/`CloseRequested` are unchanged — unification is above them.
|
||||
- **Naming alignment deferred, not done** (rationale above).
|
||||
|
||||
## Out of scope / deferred
|
||||
|
||||
- Hub/service merge-method renaming (C4).
|
||||
- Subtask deletion in the UI (a missing feature surfaced during mapping, not a duplicate).
|
||||
- Any DB migration, worker engine change, or push.
|
||||
|
||||
## Acceptance (per phase)
|
||||
|
||||
Each phase: `dotnet build -c Release` clean for touched projects; the relevant test
|
||||
project green; locales in parity (Localization.Tests) where keys change; the feature
|
||||
reachable through its single new path with the old doors removed or delegating. UI
|
||||
phases (2–5) flag a visual-verification gap for Mika to confirm in the running app.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Rider-style 3-pane merge editor (conflict resolver redesign)
|
||||
|
||||
Date: 2026-06-19
|
||||
|
||||
## Goal
|
||||
|
||||
Replace ClaudeDo's current conflict resolver (3 read-only columns Base|Ours|Theirs,
|
||||
one conflict at a time, accept buttons + editable result below) with a JetBrains
|
||||
Rider-style **3-pane merge editor**:
|
||||
|
||||
- LEFT = **Ours** (read-only) · current branch / merge target
|
||||
- MIDDLE = **Result** (editable) · the merged file being assembled
|
||||
- RIGHT = **Theirs** (read-only) · incoming task branch
|
||||
|
||||
Whole file per pane (not one conflict at a time), color-coded conflict blocks,
|
||||
inline per-hunk accept controls (`›` accept a side into the result, `✕` dismiss),
|
||||
a `M conflicts · K resolved` readout, synced scrolling, Continue gated until every
|
||||
conflict is resolved, Abort, and a binary-file guard. Visual reference: the
|
||||
attached "Merge Revisions" screenshot.
|
||||
|
||||
## Background
|
||||
|
||||
- Avalonia 12 desktop app; the conflict editor already uses **AvaloniaEdit 12.0.0**
|
||||
+ `AvaloniaEdit.TextMate` (theme `StyleInclude` in `src/ClaudeDo.App/App.axaml`).
|
||||
- **Backend is kept unchanged.** `WorkerHub.GetMergeConflictDocuments(taskId)` returns
|
||||
each conflicted file as ordered `MergeSegment`s: *stable* text (git's already
|
||||
auto-merged content) interleaved with *conflict* blocks carrying `Ours/Base/Theirs`.
|
||||
`StartConflictMerge` / `WriteConflictResolution` / `Continue[Planning]ConflictMerge` /
|
||||
`Abort[Planning]ConflictMerge` and their `IWorkerClient` mirrors stay as-is.
|
||||
`ConflictMarkerParser` (Data) already produces the segments. **ours = merge target
|
||||
(current branch); theirs = incoming task branch.** Merges are LOCAL-only (no push).
|
||||
- **Seam kept unchanged** so single-task AND planning conflict paths keep working:
|
||||
`IslandsShellViewModel.ConflictResolverFactory` + `ShowConflictResolver`
|
||||
(wired in `MainWindow.axaml.cs`), VM ctor `(IWorkerClient, taskId)`,
|
||||
`OpenAsync(targetBranch)`, `OpenForPlanningAsync(parentId, subtaskId)`, `CloseRequested`.
|
||||
The planning-path WIP currently uncommitted in the tree (`OpenForPlanningAsync`,
|
||||
`_conflictTaskId`, `LoadDocumentsAsync`) is part of this seam and is preserved.
|
||||
|
||||
### Key insight: the segments already line the panes up
|
||||
|
||||
Because every conflicted file is split into *stable* (identical on both sides, git
|
||||
auto-merged) and *conflict* (divergent) segments, reconstructing three documents —
|
||||
|
||||
- **Ours** = Σ over segments of (stable.Text | conflict.Ours)
|
||||
- **Theirs** = Σ over segments of (stable.Text | conflict.Theirs)
|
||||
- **Result** = Σ over segments of (stable.Text | conflict.Resolution ?? conflict.Ours)
|
||||
|
||||
— yields three documents that are byte-identical in their stable regions and differ
|
||||
only inside conflict blocks. So the panes align line-for-line for free, and a real
|
||||
client-side 3-way diff is **not** needed for the core feature.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Data source = segment-based (no backend change, no DiffPlex).** The worker already
|
||||
applied git's auto-merge; only conflicts remain actionable. The screenshot's
|
||||
"N changes" (non-conflicting hunks shown as separately flippable) are already merged
|
||||
and have nothing to accept, so the readout is **`M conflicts · K resolved`**. True
|
||||
"N changes" parity (raw `:1/:2/:3` blobs + DiffPlex 3-way) is an explicit later
|
||||
add-on that does not touch the seam — see *Out of scope / fast-follow*.
|
||||
- **One file at a time + file switcher.** Like Rider's title bar ("Merge Revisions for
|
||||
…file"). When more than one file conflicts, a compact switcher selects the active
|
||||
file; Continue still requires *all* files resolved. (Replaces today's cross-file
|
||||
flattened one-at-a-time navigation as the primary model.)
|
||||
- **Result-pane editing model.** The middle document is the merged file. Stable text is
|
||||
read-only via `IReadOnlySectionProvider`; only conflict regions are editable. Each
|
||||
conflict's result span is tracked in a `TextSegmentCollection` (anchors auto-adjust on
|
||||
edit). Accepting `›`(ours)/`‹`(theirs) replaces that span; editing inside it or
|
||||
accepting flips the block to **resolved**. Unresolved regions are seeded with the Ours
|
||||
text and painted red until acted on.
|
||||
- **Accept controls = overlay between panes** (not an AvaloniaEdit margin). A thin Canvas
|
||||
overlay between Ours|Result and Result|Theirs hosts `›`/`✕` (and `‹`) per conflict,
|
||||
positioned at each block's visual Y (recomputed on scroll/resize). This matches the
|
||||
screenshot's between-pane gutters and avoids the lack of a built-in right-side margin.
|
||||
- **Synced scroll = proportional (Green).** Mirror each pane's vertical scroll offset to
|
||||
the other two with a re-entrancy guard. Aligned/virtual-space scroll + bezier connector
|
||||
curves are a deferred stretch.
|
||||
- **Seam + existing VM tests preserved.** Keep `MergeConflictBlock` with its
|
||||
`AcceptOurs/Theirs/Both/Base` commands and `MergeFile.Compose`; keep
|
||||
`Current`/`CurrentIndex`/`Next`/`Previous` repurposed as the focused-conflict the top
|
||||
arrows jump to. New state (active file, readout) is additive.
|
||||
|
||||
## Architecture
|
||||
|
||||
### ViewModel (`ConflictResolverViewModel`, `ConflictModels.cs`)
|
||||
|
||||
Unchanged seam: ctor, `OpenAsync`, `OpenForPlanningAsync`, `CloseRequested`,
|
||||
`Continue`/`Abort` (incl. planning routing), `CanContinue` gating, binary guard.
|
||||
|
||||
Additive:
|
||||
- `ActiveFile` (`MergeFile`) + the switcher list (`Files`) + `SelectFileCommand`.
|
||||
- Per-active-file reconstruction exposed for the view and for tests:
|
||||
`ActiveOursText`, `ActiveTheirsText`, `ActiveResultText` (result seeds unresolved =
|
||||
Ours), plus an ordered list of conflict descriptors (the block + its segment index)
|
||||
so the view can compute offsets/spans as it assembles each document.
|
||||
- Readout `PositionText` → `"{M} conflicts · {K} resolved"` (active file and/or total);
|
||||
`CanContinue` stays "all files resolved AND no binary".
|
||||
- On switching files, block `Resolution` persists (state lives on `MergeConflictBlock`),
|
||||
so progress survives navigation; the view rebuilds documents from the active file.
|
||||
|
||||
### View (`Views/Conflicts/ConflictResolverView.axaml` + `.cs`)
|
||||
|
||||
- AXAML: ModalShell host (kept), header (prev/next arrows, file switcher, readout),
|
||||
`Grid` of three bordered panes with headers, two between-pane overlay Canvases,
|
||||
footer (Continue/Abort), binary banner, `Escape`→Abort. Drop the Base column.
|
||||
- Code-behind builds three `TextDocument`s from `ActiveFile`'s segments, recording each
|
||||
conflict's line span per document; installs TextMate by file extension on all three;
|
||||
rebuilds on file switch; pushes result-pane edits back into the active block's
|
||||
`Resolution` and flips resolved.
|
||||
- `IReadOnlySectionProvider` on the Result `TextArea` (stable = read-only, conflicts =
|
||||
editable) backed by a `TextSegmentCollection` of the conflict result-spans.
|
||||
- One `IBackgroundRenderer` per pane painting unresolved-conflict (red), resolved
|
||||
(green/muted), and ours/theirs side tints, driven by the recorded spans + block state.
|
||||
- Overlay accept controls positioned at each block's `TextView` visual top; click →
|
||||
`block.AcceptOurs/AcceptTheirs` and the code-behind replaces the tracked result span.
|
||||
- Proportional synced vertical scroll across the three panes.
|
||||
|
||||
### Localization / tokens
|
||||
|
||||
- New `conflictResolver.*` keys (pane headers, readout, accept tooltips) in
|
||||
`en.json` + `de.json` (parity enforced by Localization.Tests).
|
||||
- Block colors from `Tokens.axaml` (reuse Blood/Moss/Accent tints; add tokens only if a
|
||||
needed shade is missing).
|
||||
|
||||
## Out of scope / fast-follow (not in this plan)
|
||||
|
||||
- **Raw 3-way diff "N changes" parity (Option B):** a new worker method returning raw
|
||||
`:1/:2/:3` blobs per conflicted file + DiffPlex client-side 3-way diff so
|
||||
non-conflicting changes also appear as accept-able hunks. Seam-preserving; later.
|
||||
- **Intra-conflict word/line highlighting** (Rider's "Highlight words") via a line
|
||||
transformer.
|
||||
- **Bezier connector curves + aligned / virtual-space synced scroll** (Red stretch).
|
||||
- No DB migration, no backend/seam changes, no push.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Worker log → footer auto-route + Log Visualizer overlay
|
||||
|
||||
**Date:** 2026-06-23
|
||||
**Status:** approved (design forks resolved with user)
|
||||
|
||||
## Goal
|
||||
|
||||
1. Auto-route **all Worker WARN/ERROR** Serilog events to the footer status strip (today only ~10 hand-curated business events reach it).
|
||||
2. Make the footer log line **clickable** → opens a **Log Visualizer overlay** showing the **last 30 min** of logs at **all levels**, color-coded.
|
||||
3. **Dedupe/rate-limit** the footer so repeating warnings (e.g. the current 60s OIDC-discovery failure) don't strobe.
|
||||
|
||||
## Decisions (locked)
|
||||
|
||||
- **Overlay source:** Worker-side **in-memory ring buffer** (30-min window, all levels), fetched via a hub call. No log-file parsing.
|
||||
- **Levels:** overlay shows INF/WRN/ERR; footer flashes **WARN/ERROR only**.
|
||||
- **Footer noise:** per-message dedupe within a rate-limit window (suppress the footer broadcast for an identical message seen recently; the event is still buffered for the overlay).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Worker
|
||||
|
||||
- **`LogRingBuffer`** (singleton, `Logging/`): thread-safe, time-bounded (`TimeSpan` window, default 30 min) + hard cap (e.g. 5000) ring of `WorkerLogRecord(Message, Level, TimestampUtc)`. Evicts on append by age + cap. `Snapshot()` returns newest-last.
|
||||
- **`BroadcastLogSink : Serilog.Core.ILogEventSink`** (`Logging/`): for every `LogEvent` —
|
||||
- map level: Verbose/Debug/Information→`Info`, Warning→`Warn`, Error/Fatal→`Error`;
|
||||
- render `msg = evt.RenderMessage()` (+ `": {ex.GetType().Name}: {ex.Message}"` first-line if `evt.Exception != null`);
|
||||
- append to `LogRingBuffer` (all levels);
|
||||
- if `Warn|Error` **and** not rate-limited: fire-and-forget `HubBroadcaster.WorkerLog(msg, level, evt.Timestamp.UtcDateTime)`.
|
||||
- **Loop guard:** wrap the broadcast in try/catch and swallow; skip broadcasting events whose `SourceContext` is SignalR/connections plumbing (still buffered). Broadcasting must never itself log.
|
||||
- **Dedupe/rate-limit:** dict `message → lastBroadcastUtc`; suppress footer broadcast if `now - last < RateLimitWindow` (const, 120 s). Periodic prune of the dict.
|
||||
- **DI wiring (chicken-egg):** `LogRingBuffer` + `BroadcastLogSink` are created as locals in `Program.cs` *before* `builder.Build()`, captured into `UseSerilog(... .WriteTo.Sink(broadcastSink))`, and registered as singletons. `HubBroadcaster` doesn't exist until post-build, so the sink starts detached; after `builder.Build()` we call `broadcastSink.Attach(app.Services.GetRequiredService<HubBroadcaster>())`. Buffering works from process start; broadcasting begins once attached.
|
||||
- **Hub:** `WorkerHub.GetRecentLogs() -> IReadOnlyList<WorkerLogRecordDto>` reads `LogRingBuffer.Snapshot()`. (Read-only, no auth beyond existing hub.)
|
||||
|
||||
### UI
|
||||
|
||||
- **IWorkerClient / WorkerClient:** add `Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync(CancellationToken ct = default)`. ⚠ Update hand-rolled fakes in **both** test projects (StubWorkerClient + Worker.Tests UiVm fake).
|
||||
- **Footer:** wrap the worker-log `TextBlock` so it's clickable (Button, transparent) → `IslandsShellViewModel.OpenLogVisualizerCommand`. Existing `OnWorkerLogReceived` already routes the (now more numerous) `WorkerLog` broadcasts to the strip — **no change needed** for footer routing itself.
|
||||
- **`LogVisualizerViewModel`** (Modals/): on open, `GetRecentLogsAsync()` → `ObservableCollection<LogLineViewModel>` (msg, level→brush, HH:mm:ss). A level filter (All / Warn+Err) and a Refresh command. MVP = snapshot on open + Refresh; live-tail is a later nicety.
|
||||
- **`LogVisualizerView`** (Modals/): `ModalShell`-based dialog (consistent with other modals), shown via `IDialogService.ShowLogVisualizerAsync(vm)`. Small, scrollable, monospaced, color-coded lines.
|
||||
- **Localization:** new `vm.logVisualizer` (+ any view keys) in **en.json + de.json** (parity test enforces).
|
||||
|
||||
## Out of scope / follow-ups
|
||||
|
||||
- Live-tail while the overlay is open (snapshot + Refresh for MVP).
|
||||
- The **OIDC-discovery-every-60s failure** is a *separate* bug (Online Inbox enabled, `auth.kuns.dev` SSL fails). Dedupe tames the footer symptom; the root cause is tracked separately.
|
||||
|
||||
## Tests
|
||||
|
||||
- Worker: `LogRingBufferTests` (age + cap eviction, snapshot order), `BroadcastLogSinkTests` (level mapping; all levels buffered; only Warn/Err broadcast; dedupe suppresses repeat broadcast within window but still buffers; exception rendering; loop-guard source filter).
|
||||
- UI: `LogVisualizerViewModelTests` (loads from worker, populates, filter). Footer-click wiring smoke.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Interactive "Answer Claude's Questions" — Design
|
||||
|
||||
**Date:** 2026-06-25
|
||||
**Status:** Approved (brainstormed with Mika)
|
||||
|
||||
## Goal
|
||||
|
||||
Let the user answer a question Claude raises *mid-run* from inside Mission Control,
|
||||
without leaving the autonomous-execution model. Not a chat panel, not a terminal, not
|
||||
proactive steering — only: *Claude surfaces a question → the user types an answer → the
|
||||
run continues with that answer in context.*
|
||||
|
||||
User decisions (brainstorm):
|
||||
- Scope: "I mostly want to answer his questions if he surfaces any."
|
||||
- Trigger: **any running task** may ask, with a **3-minute** answer window.
|
||||
|
||||
## Why not the alternatives
|
||||
|
||||
- **Embedded terminal / PTY** — would destroy the NDJSON contract the whole worker
|
||||
pipeline depends on (StreamAnalyzer, token accounting, auto-commit, status flow) and
|
||||
needs a terminal-emulator control Avalonia doesn't have. Rejected.
|
||||
- **Streaming-stdin (`--input-format stream-json`)** — right tool for a free-form chat,
|
||||
overkill here. Rejected for v1.
|
||||
- **`--resume` per-turn** — already exists; not live (cold process per turn).
|
||||
|
||||
## Mechanism
|
||||
|
||||
The in-task MCP already blocks the `claude -p` process while a tool call is in flight.
|
||||
That blocking *is* the pause. Add one in-task MCP tool, `AskUser(question)`:
|
||||
|
||||
1. The tool resolves the caller task id, registers a pending question + a
|
||||
`TaskCompletionSource<string>` in a singleton `PendingQuestionRegistry`, and
|
||||
broadcasts `TaskQuestionAsked(taskId, questionId, question)`.
|
||||
2. Mission Control surfaces the question with an input box.
|
||||
3. The user answers → `WorkerHub.AnswerTaskQuestion` resolves the TCS → the tool
|
||||
returns the answer as its result → Claude continues.
|
||||
4. No answer within **3 minutes** → the tool returns *"No response received within 3
|
||||
minutes — proceed using your best judgment."* and the run carries on autonomously.
|
||||
|
||||
### Key facts that make this work
|
||||
|
||||
- **No persisted status change.** The task is still genuinely `Running` (process alive,
|
||||
blocked mid-tool-call). "Waiting for input" is **ephemeral**: in-memory registry +
|
||||
live SignalR events + a UI overlay. No `TaskStatus` enum value, no `TaskStateService`
|
||||
transition, **no EF migration**. If the worker dies mid-wait, `StaleTaskRecovery`
|
||||
flips the orphaned `Running` row to `Failed` like any interrupted run.
|
||||
- **`MCP_TOOL_TIMEOUT` must be raised.** Claude Code caps HTTP MCP tool calls at **60 s**
|
||||
by default. The `claudedo_run` MCP is HTTP, so `ClaudeProcess` must set
|
||||
`MCP_TOOL_TIMEOUT=200000` (≈3 min + margin) on the spawned process or the 3-min window
|
||||
is silently truncated to 60 s.
|
||||
- **MCP wired for all runs.** Today `TaskRunner` only mints the run MCP for standalone
|
||||
top-level tasks (for `SuggestImprovement`). To satisfy "any running task," move the
|
||||
MCP-identity setup out of that gate so every `RunAsync` gets `claudedo_run`.
|
||||
`AllowedTools` always includes `mcp__claudedo_run__AskUser`; `SuggestImprovement` stays
|
||||
gated to improvement-eligible (standalone) runs.
|
||||
|
||||
## Surface changes
|
||||
|
||||
**Worker (mostly new files):**
|
||||
- `Runner/PendingQuestionRegistry.cs` (new, singleton) — `Register`, `TryAnswer`, `Get`,
|
||||
`Remove`; one pending question per task.
|
||||
- `Runner/TaskRunMcpService.cs` (edit) — add `AskUser` `[McpServerTool]`; inject the
|
||||
registry.
|
||||
- `Runner/TaskRunner.cs` (edit) — wire MCP identity for all runs; add `AskUser` to
|
||||
allowed tools.
|
||||
- `Runner/ClaudeProcess.cs` (edit) — set `MCP_TOOL_TIMEOUT` env.
|
||||
- `Hub/HubBroadcaster.cs` (edit) — `TaskQuestionAsked`, `TaskQuestionResolved`.
|
||||
- `Hub/WorkerHub.cs` (edit) — `AnswerTaskQuestion`, `GetPendingQuestion` + DTO.
|
||||
- `Program.cs` (edit) — register `PendingQuestionRegistry` singleton.
|
||||
- System prompt (edit) — one line telling Claude the tool exists and to use it only when
|
||||
a wrong guess would be costly/irreversible (otherwise proceed).
|
||||
|
||||
**UI:**
|
||||
- `Services/IWorkerClient.cs` + `WorkerClient.cs` (edit) — `AnswerTaskQuestionAsync`,
|
||||
`GetPendingQuestionAsync`, `TaskQuestionAskedEvent`, `TaskQuestionResolvedEvent`.
|
||||
- `ViewModels/Islands/TaskMonitorViewModel.cs` (edit, **hot file**) — pending-question
|
||||
state, `AnswerDraft`, `SubmitAnswerCommand`, clear on finish/resolve.
|
||||
- `ViewModels/MissionControlViewModel.cs` (edit) — hydrate pending question on attach.
|
||||
- `Views/MissionControl/MonitorPaneView.axaml` (edit, **hot file**) — additive
|
||||
question/answer banner above the terminal.
|
||||
- `Localization/locales/en.json` + `de.json` — `missionControl.question.*` keys.
|
||||
|
||||
**Tests:** `PendingQuestionRegistry` (answer/timeout/unknown/overwrite), `AskUser` tool
|
||||
(answer + timeout fallback, fake broadcaster — no real Claude), `TaskMonitorViewModel`
|
||||
(surface/submit/clear). Update IWorkerClient fakes in both test projects.
|
||||
|
||||
## Concurrency note
|
||||
|
||||
Two files (`TaskMonitorViewModel.cs`, `MonitorPaneView.axaml`) are also being touched by
|
||||
a concurrent Mission Control drag-and-drop session on the shared main tree. Keep edits
|
||||
additive, commit explicit paths only (never `git add -A`).
|
||||
|
||||
## Verification gaps (manual)
|
||||
|
||||
1. **Real-Claude smoke test** — confirm a blocking `AskUser` call survives ≥3 min with
|
||||
`MCP_TOOL_TIMEOUT=200000` and that the model actually calls the tool when uncertain.
|
||||
2. **Visual** — the question banner + input box in the pane (Mika does the visual pass).
|
||||
|
||||
## Non-goals
|
||||
|
||||
Free-form chat panel; proactive steering; tool-permission prompts (stays `auto`);
|
||||
`ContinueAsync`/resumed runs gaining `AskUser` (deferred follow-up).
|
||||
@@ -0,0 +1,144 @@
|
||||
# Mission Control — multi-task live monitoring
|
||||
|
||||
Date: 2026-06-25
|
||||
Status: approved (design); implementation not started
|
||||
|
||||
## Problem
|
||||
|
||||
The UI can observe only **one** running task at a time. `DetailsIslandViewModel` is hard 1:1
|
||||
(single `Task`, single `_subscribedTaskId`); selecting another task in the middle pane *replaces*
|
||||
what Details shows. Yet the worker runs several tasks concurrently (`MaxParallelExecutions`) and
|
||||
already broadcasts every task's live output to all clients keyed by `taskId`. So the user cannot
|
||||
watch multiple in-flight sessions, and monitoring blocks normal work (adding tasks, reviewing).
|
||||
|
||||
## Goal
|
||||
|
||||
Watch several running tasks at once **without** giving up the normal app. Requirements drawn from
|
||||
the brainstorm:
|
||||
|
||||
- A **live console grid** — multiple full Claude output streams side by side.
|
||||
- Each pane also shows **task details, blocking reasons**, and a **navigation helper** to open the
|
||||
monitored task in the main app.
|
||||
- Lives in a **separate, always-available window** so the main window stays fully usable (adding
|
||||
tasks must never be blocked). Combines "full window" + "detachable".
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No worker/SignalR changes. The broadcast layer is already N-capable (`TaskMessage(taskId,line)`,
|
||||
`TaskStarted/Finished/Updated`, `GetActive()`). This is a UI/VM-only feature.
|
||||
- No second SignalR connection. The new window shares the existing singleton `IWorkerClient`.
|
||||
- No new merge/review engine. Review/merge stays in the main window's Details pane; Mission Control
|
||||
is read-mostly (monitor + cancel + navigate).
|
||||
|
||||
## Hard constraint: no duplicated components or features
|
||||
|
||||
This feature is an **extract-and-reuse** exercise, not a rebuild. The single biggest risk is
|
||||
forking a second live-streaming/parsing/status implementation. The reuse map below is binding.
|
||||
|
||||
### Reuse map (what already exists — use it, do not copy it)
|
||||
|
||||
| Concern | Existing asset | Location | How Mission Control uses it |
|
||||
|---|---|---|---|
|
||||
| Live console body (log list, LIVE/DONE/FAILED chip, auto-scroll) | `SessionTerminalView` (StyledProps `Entries`, `Label`, `IsRunning/IsDone/IsFailed`) | `Views/Islands/SessionTerminalView.axaml(.cs)` | Bind a pane's `Entries`→its `Log`, status flags + label. **No new console control.** |
|
||||
| Log line model | `LogLineViewModel` + `LogKind` | `ViewModels/Islands/DetailsIslandViewModel.cs` (top) | Shared model — move to its own file so both consumers reference one type. |
|
||||
| Live stream parse/replay | `OnTaskMessage` / `AppendStdoutLine` / `FlushClaudeBuffer` / `ReplayLogFileAsync` + `StreamLineFormatter` + `ExpandUserPath` | private in `DetailsIslandViewModel.cs` | **Extract to `TaskMonitorViewModel`** (Phase 1). One streaming engine, two consumers. |
|
||||
| Status state machine | `AgentState` + `Is*` flags + `StatusToStateKey` / `FinishedStatusToStateKey` | `DetailsIslandViewModel.cs` | Extract into `TaskMonitorViewModel`. |
|
||||
| Outcome / roadblock split | `ApplyOutcome` + `RoadblockMarker` constant | `DetailsIslandViewModel.cs` | Extract into `TaskMonitorViewModel`. |
|
||||
| Status chip / terminal styling | `live-chip`, `terminal`, `log-*` style classes | `Design/IslandStyles.axaml` | Reuse the classes as-is. |
|
||||
| Add a new task | `TasksIslandViewModel.AddAsync` (`NewTaskTitle`, user-list only, direct `TaskRepository`) | `TasksIslandViewModel.cs:406` | Optional quick-add reuses this path; **must not** introduce a second insert path. |
|
||||
| Live task list | `IWorkerClient.GetActive()` + `TaskStarted/Finished` events | worker hub / `WorkerClient` | Populate the grid; add/remove panes. |
|
||||
| DI / singletons | `IslandsShellViewModel`, `DetailsIslandViewModel`, `IWorkerClient` all singletons | `App/Program.cs` | Register `MissionControlViewModel` singleton; inject existing singletons. |
|
||||
|
||||
## Design
|
||||
|
||||
### TaskMonitorViewModel (the reusable core — new, but carved out of DetailsIslandViewModel)
|
||||
|
||||
One instance == one monitored task. Owns:
|
||||
|
||||
- `Log` (`ObservableCollection<LogLineViewModel>`), the filtered `TaskMessageEvent` subscription
|
||||
(by `taskId`), stdout buffering, and NDJSON replay from disk on attach.
|
||||
- `AgentState` + `Is*` flags; `SessionOutcome` / `Roadblocks` (the outcome split).
|
||||
- Lightweight display: `Title`, `TaskIdBadge`, `Model`, `TurnsText`, `TokensFormatted`,
|
||||
diff add/del, elapsed.
|
||||
- `BlockingReason` (string/visible flag) derived from existing data: `BlockedByTaskId`
|
||||
(planning/child chain), `WaitingForReview` / `WaitingForChildren` status, and roadblock markers.
|
||||
- Commands: `OpenInApp`, `Detach`, `Cancel`.
|
||||
- `IDisposable` — unsubscribes all worker events (mirror DetailsIslandViewModel.Dispose).
|
||||
|
||||
`DetailsIslandViewModel` is refactored to **own one `TaskMonitorViewModel` (`public Monitor`)** and
|
||||
delegate streaming/status/outcome to it. Its heavy concerns (subtasks, attachments, editing, merge
|
||||
cockpit, review verbs, child outcomes, notes/prep modes) stay put. **Phase 1 must be a no-behavior-
|
||||
change refactor** — all existing Ui.Tests stay green.
|
||||
|
||||
> Binding-surface decision (Phase 1): repoint `WorkConsole.axaml`'s Output-tab bindings that
|
||||
> reference streaming/status (`Log`, `IsRunning/IsDone/IsFailed`, `SessionOutcome`, `TurnsText`,
|
||||
> diff text, `Model`) to `Monitor.*`. `x:DataType` stays `DetailsIslandViewModel`; compiled bindings
|
||||
> handle the nested path. Review/merge/session bindings are untouched. Prefer repointing over adding
|
||||
> ~15 forwarding properties (one source of truth, no boilerplate).
|
||||
|
||||
### MissionControlViewModel (new)
|
||||
|
||||
- `ObservableCollection<TaskMonitorViewModel> Monitors`, keyed by `taskId`.
|
||||
- On open: seed from `GetActive()`. On `TaskStarted`: add a monitor. On `TaskFinished`: keep the
|
||||
pane (so the final output stays readable) but flip its state; a "clear finished" action prunes them.
|
||||
- Adaptive layout signal (column count) from `Monitors.Count`:
|
||||
`1→1col, 2→2col, 3–4→2col(2 rows), 5+→fixed-width panes, horizontal scroll`. Least-active panes
|
||||
beyond a threshold collapse to a compact card (title + last line + chip), click to expand — this is
|
||||
the readability fallback so we never render N unreadable slivers.
|
||||
- Optional `QuickAdd` (deferred within Phase 2): title + target user-list → the **same** creation
|
||||
path as `TasksIslandViewModel.AddAsync` (shared method, not a copy).
|
||||
- Disposes every monitor on window close.
|
||||
|
||||
### Windowing (new plumbing — thin)
|
||||
|
||||
- `MissionControlWindow` (Avalonia `Window`) hosting `MissionControlView`; DataContext =
|
||||
the singleton `MissionControlViewModel`.
|
||||
- No non-modal secondary-window precedent exists (all current dialogs use `ShowDialog(owner)`), so
|
||||
this is genuinely new but small:
|
||||
- Set `desktop.ShutdownMode = OnMainWindowClose` in `App.OnFrameworkInitializationCompleted` so
|
||||
closing Mission Control never quits the app, and closing the main window does.
|
||||
- Open via a **title-bar button in MainWindow** (toggle: show / focus-if-open). The window is
|
||||
created lazily and hidden (not destroyed) on close so its monitors persist cheaply.
|
||||
- Persist size/position (reuse the ui.config.json mechanism if present; otherwise defer).
|
||||
|
||||
### MonitorPaneView (new view, reuses SessionTerminalView)
|
||||
|
||||
```
|
||||
┌─ #142 Refactor auth module ───────── ● running ─┐ header: title, live chip, tok/turn/elapsed
|
||||
│ ⏱ 4m12s ◆ 18.3k tok ↻ turn 6 │
|
||||
├───────────────────────────────────────────────────┤
|
||||
│ ⚠ Blocked: waiting on #141 (planning parent) │ blocking banner (visible only when blocked)
|
||||
├───────────────────────────────────────────────────┤
|
||||
│ <SessionTerminalView Entries={Log} .../> │ the REUSED console
|
||||
├───────────────────────────────────────────────────┤
|
||||
│ [↗ Open in app] [⧉ Detach] [✕ Cancel] │ footer
|
||||
└───────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Navigation helper "Open in app" (new shell method)
|
||||
|
||||
No select-by-id exists today. Add `IslandsShellViewModel.RevealTaskAsync(taskId)`:
|
||||
1. resolve the task's list, set `Lists.SelectedList`; 2. await `Tasks.LoadForList`; 3. find the row in
|
||||
`Tasks.Items` by id, set `Tasks.SelectedTask` (→ `Details.Bind`); 4. bring MainWindow to front.
|
||||
`TaskMonitorViewModel.OpenInApp` calls this. Single navigation entry point — no duplicate selection logic.
|
||||
|
||||
### Detach (Phase 3)
|
||||
|
||||
`Detach` moves a `TaskMonitorViewModel` out of the grid into a small `TaskMonitorWindow`
|
||||
(reuses `MonitorPaneView`), optionally always-on-top; closing it re-docks. Lowest priority.
|
||||
|
||||
## Risks / open items
|
||||
|
||||
- **Phase 1 binding repoint** is the main risk: a missed `WorkConsole` binding shows as a blank
|
||||
field, not a build error. Mitigation: Ui.Tests + a manual visual pass on the Details pane.
|
||||
- **Localization parity** (Localization.Tests): every new visible string needs en + de keys under a
|
||||
`missionControl.*` namespace.
|
||||
- **Quick-add coupling** across windows is the weakest part; kept optional/deferrable.
|
||||
- Detached windows = most plumbing, least daily payoff → Phase 3, last.
|
||||
|
||||
## Verification
|
||||
|
||||
- Build `ClaudeDo.App` + run Ui.Tests / Localization.Tests after each phase.
|
||||
- Manual visual pass (cannot be auto-verified): Details pane unchanged after Phase 1; grid populates
|
||||
with 2+ concurrent tasks, blocking banner shows, Open-in-app surfaces the task, adding a task in the
|
||||
main window works while Mission Control is open.
|
||||
@@ -0,0 +1,147 @@
|
||||
# In-App Interactive Sessions — Design
|
||||
|
||||
**Date:** 2026-06-26
|
||||
**Status:** Proposed (awaiting approval)
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the external Windows-Terminal "Run interactively" session with an **in-app
|
||||
streaming chat**, rendered in the existing `SessionTerminalView` in **both task detail and
|
||||
Mission Control**. Keep everything inside the app — no `wt.exe` pop-out. Autonomous task
|
||||
execution is **untouched** (stays one-shot, non-interactive).
|
||||
|
||||
## Decisions (brainstorm)
|
||||
|
||||
1. **Engine: persistent streaming session.** One `claude` process kept alive with
|
||||
`--input-format stream-json`; user messages pushed over stdin.
|
||||
2. **Scope: interactive sessions only.** The autonomous `TaskRunner`/`ClaudeProcess` run
|
||||
loop, review, queue, and worktree machinery are NOT changed.
|
||||
3. **Placement: shared `SessionTerminalView`** — the in-app session + composer appear in the
|
||||
task-detail session surface and in the Mission Control monitor pane.
|
||||
4. **Full replace.** "Run interactively" now opens the in-app session; the
|
||||
`WindowsTerminalLauncher.LaunchInteractiveAsync` path is removed. **Planning** sessions
|
||||
keep using `wt` (untouched).
|
||||
5. **Send semantics: interrupt + redirect** mid-turn (control protocol), with automatic
|
||||
*queue-for-next-turn* fallback if interrupt is unavailable.
|
||||
|
||||
## What an interactive session is (unchanged semantics, new transport)
|
||||
|
||||
Today (`PlanningSessionManager.OpenInteractiveAsync` + `WindowsTerminalLauncher`):
|
||||
`claude --model <PlanningAlias> --permission-mode auto "<task title+description>"` in the
|
||||
**list's working dir**, env `MAX_THINKING_TOKENS=20000`, full default toolset, relies on the
|
||||
globally-registered `claudedo` MCP. **Ephemeral** — no worktree, no `task_run` record, no
|
||||
status change, no review.
|
||||
|
||||
We keep all of that. Only the transport changes: instead of a `wt` window, the same
|
||||
`claude` invocation runs as a persistent stream-json process owned by the worker, its output
|
||||
streamed into the app and its stdin fed from an in-app composer.
|
||||
|
||||
> Honest tradeoff: the `wt` terminal gave the full Claude Code TUI (slash-command UX,
|
||||
> interactive prompts). An in-app stream-json chat is plainer — type messages, watch streamed
|
||||
> output. `--permission-mode auto` means no blocking permission prompts (so headless works),
|
||||
> but it is a simpler surface than the real TUI. Accepted per the "full replace" decision.
|
||||
|
||||
## The streaming engine
|
||||
|
||||
Flags: `--model <PlanningAlias> --permission-mode auto --input-format stream-json
|
||||
--output-format stream-json --verbose --replay-user-messages` in the list working dir, env
|
||||
`MAX_THINKING_TOKENS=20000`. No `--mcp-config`/`--allowedTools` (interactive uses the global
|
||||
MCP + default tools, exactly as today).
|
||||
|
||||
- First stdin message = the seeded interactive prompt:
|
||||
`{"type":"user","message":{"role":"user","content":[{"type":"text","text":"…"}]},"parent_tool_use_id":null}\n`
|
||||
(stdin stays open).
|
||||
- A stdout read task forwards each NDJSON line to a callback (→ broadcast + the session's log)
|
||||
and detects `result` events (turn boundary; the process then idles for the next message).
|
||||
- `SendUserMessageAsync(text)` writes a user-message JSON line; if a turn is in flight, also
|
||||
`InterruptAsync()` (control-protocol interrupt) so Claude pivots immediately. If interrupt
|
||||
is unavailable, the message lands when the current turn ends → automatic queue fallback.
|
||||
- **Interrupt is verified working** (spike, 2026-06-26, CLI 2.1.191). Exact shape:
|
||||
`{"type":"control_request","request_id":"<id>","request":{"subtype":"interrupt"}}` — no
|
||||
`initialize` handshake needed; `control_response {"subtype":"success"}` confirms
|
||||
synchronously; the same process then accepts the redirect and runs a fresh turn with
|
||||
context intact.
|
||||
- **Interrupt artifact:** the aborted turn emits a `result` with `is_error=true,
|
||||
subtype="error_during_execution"`. The session must treat an interrupt-induced result as
|
||||
*"turn aborted, continue"* (drain the queued redirect), **not** as a session failure.
|
||||
Tolerate the incidental `system:init`/`system:status`/`rate_limit_event`/hook events that
|
||||
also appear in the stream.
|
||||
- `--replay-user-messages` echoes each sent message back on stdout as a `user` event, so it
|
||||
rides the existing stream pipeline into the timeline (ordered + confirmed) with no extra
|
||||
broadcast surface.
|
||||
- The session ends only when the **user stops it** (kill the process tree) — an interactive
|
||||
session has no auto-finalize and never enters review. No queue slot is involved (it is
|
||||
launched directly, not via the autonomous picker).
|
||||
|
||||
## Surface changes
|
||||
|
||||
**Worker**
|
||||
- `Runner/StreamingClaudeSession.cs` (new) — persistent process + send/interrupt/stop; reuse
|
||||
the `ProcessStartInfo` shape + `MCP_TOOL_TIMEOUT` from `ClaudeProcess`; streams via a line
|
||||
callback; `IsTurnInFlight`. Cancellation kills the tree.
|
||||
- `Runner/LiveSessionRegistry.cs` (new, singleton) — `taskId → StreamingClaudeSession`
|
||||
(`Register`/`TryGet`/`Unregister`/`Stop`), mirrors `PendingQuestionRegistry`.
|
||||
- `Planning/InteractiveSessionService.cs` (new) — owns interactive lifecycle: `StartAsync(
|
||||
taskId)` resolves the list working dir + seeded prompt (reuse `OpenInteractiveAsync`'s
|
||||
body), spawns the session, registers it, wires output to `HubBroadcaster.TaskMessage`,
|
||||
broadcasts `InteractiveSessionStarted`; `SendAsync(taskId, text)`; `StopAsync(taskId)` →
|
||||
`InteractiveSessionEnded`.
|
||||
- `Planning/WindowsTerminalLauncher.cs` + `Planning/Interfaces/ITerminalLauncher.cs` — remove
|
||||
`LaunchInteractiveAsync` (+ `InteractiveLaunchContext`). Planning start/resume stay.
|
||||
- `Hub/WorkerHub.cs` — `OpenInteractiveTerminalAsync` re-pointed to
|
||||
`InteractiveSessionService.StartAsync` (no terminal); add `SendInteractiveMessage(taskId,
|
||||
text)`, `StopInteractiveSession(taskId)` (+ optional `InterruptInteractiveSession`).
|
||||
- `Hub/HubBroadcaster.cs` — `InteractiveSessionStarted(taskId)`,
|
||||
`InteractiveSessionEnded(taskId)`. Log lines reuse the existing `TaskMessage(taskId, line)`.
|
||||
- `Program.cs` — register `LiveSessionRegistry` + `InteractiveSessionService`.
|
||||
|
||||
**UI**
|
||||
- `Views/Islands/SessionTerminalView.axaml(.cs)` — add an optional composer (styled
|
||||
properties: `IsComposerVisible`, `ComposerText`, `SubmitCommand`, `ComposerPlaceholder`).
|
||||
Both hosts (task detail + Mission Control) get it by binding their VM's composer state.
|
||||
- `StreamLineFormatter` — render `type:"user"` NDJSON events as a `LogKind.User` bubble.
|
||||
- A small shared composer concept on `TaskMonitorViewModel` **and** `DetailsIslandViewModel`
|
||||
(factor a helper to avoid duplication): `ComposerDraft`, `SubmitComposerCommand`,
|
||||
`IsInteractiveLive` (set by `InteractiveSessionStarted/Ended`). Submit →
|
||||
`SendInteractiveMessageAsync`; clear draft. (If a pending AskUser question exists, the same
|
||||
composer answers it — keep the existing answer route.)
|
||||
- `MissionControlViewModel` — `EnsureMonitor(taskId)` on `InteractiveSessionStarted` so the
|
||||
session appears as a monitor; mark it interactive.
|
||||
- `Services/Interfaces/IWorkerClient.cs` + `WorkerClient.cs` — `SendInteractiveMessageAsync`,
|
||||
`StopInteractiveSessionAsync` (+ optional interrupt); events
|
||||
`InteractiveSessionStartedEvent`/`InteractiveSessionEndedEvent`. `OpenInteractiveTerminalAsync`
|
||||
keeps its name/signature (now starts the in-app session). Update hand-rolled fakes in **both**
|
||||
test projects (`iworkerclient_fakes_sync`).
|
||||
- `TasksIslandViewModel.RunInteractivelyAsync` — unchanged call site; now opens/focuses the
|
||||
in-app session surface instead of a terminal.
|
||||
- Localization `interactive.*` / `missionControl.chat.*` (en/de, parity enforced).
|
||||
|
||||
**Tests**
|
||||
- `StreamingClaudeSessionTests` (fake process stream, no real Claude): first message streams;
|
||||
`result` idles; a sent message starts another turn; mid-turn send calls `InterruptAsync`
|
||||
then delivers; interrupt-failure degrades to queue; stop kills.
|
||||
- `LiveSessionRegistryTests` — register/get/unregister/stop.
|
||||
- `InteractiveSessionServiceTests` — start resolves working dir + seeds prompt + registers +
|
||||
broadcasts started; send routes to the session; stop broadcasts ended (fake session +
|
||||
broadcaster).
|
||||
- `TaskMonitorViewModelTests` / `DetailsIslandViewModelTests` — composer enabled while
|
||||
interactive-live; submit invokes client + clears; `user` line renders; question route still
|
||||
answers.
|
||||
|
||||
## Risks / open questions
|
||||
|
||||
- **Interrupt protocol shape — RESOLVED** (spike 2026-06-26, see "The streaming engine").
|
||||
Mid-turn interrupt works on CLI 2.1.191 with the documented shape; the queue fallback is a
|
||||
genuine fallback now, not the expected path. Re-verify if the CLI version changes.
|
||||
- **Plainer than the TUI** — slash-command/interactive-prompt UX differs (accepted).
|
||||
- **Auto-mode editing the list working dir directly** (no worktree) — this is the *existing*
|
||||
interactive behavior, unchanged here.
|
||||
- **No real-Claude tests** (project rule) — the live loop is covered only by the fake stream;
|
||||
real interrupt/redirect is a **manual verification gap** to flag.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing autonomous task execution / review / queue / worktrees.
|
||||
- Interactive sessions producing run records, worktrees, or review (stays ephemeral).
|
||||
- Worktree isolation for interactive edits; image/attachment messages in the composer.
|
||||
- Removing planning's `wt` terminal launch.
|
||||
@@ -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,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.
|
||||
@@ -21,6 +21,7 @@
|
||||
<converters:DotBrushConverter x:Key="DotBrush"/>
|
||||
<converters:BoolToItalicConverter x:Key="BoolToItalic"/>
|
||||
<converters:BoolToDraftOpacityConverter x:Key="BoolToDraftOpacity"/>
|
||||
<converters:LogKindForegroundConverter x:Key="LogKindForeground"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
@@ -31,6 +32,7 @@
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />
|
||||
<!-- Global defaults: every Window inherits Inter Tight + body size.
|
||||
Controls that need mono opt in via their own class/style. -->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using ClaudeDo.Ui.Services;
|
||||
@@ -32,6 +33,10 @@ public partial class App : Application
|
||||
|
||||
FocusClearing.Install();
|
||||
|
||||
// The main window is authoritative — closing it shuts the app down even if the
|
||||
// modeless Mission Control window is still open.
|
||||
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = services.GetRequiredService<IslandsShellViewModel>(),
|
||||
|
||||
@@ -19,8 +19,8 @@ Desktop entry point for the ClaudeDo application. Configures DI, initializes the
|
||||
|
||||
## DI Registration Pattern
|
||||
|
||||
- **Singletons**: `IDbContextFactory`, all Repositories, GitService, WorkerClient, `IReleaseClient`, `InstallerLocator` / `WorkerLocator`, the island VMs (`ListsIslandViewModel`, `TasksIslandViewModel`, `DetailsIslandViewModel`) and `IslandsShellViewModel` (the window's DataContext)
|
||||
- **Transients**: modal VMs (`SettingsModalViewModel`, `MergeModalViewModel`, `ListSettingsModalViewModel`, `RepoImportModalViewModel`, `WorktreeModalViewModel`, `WorktreesOverviewModalViewModel`, `PrimeClaudeTabViewModel`), several exposed as `Func<T>` factories for on-demand dialog creation
|
||||
- **Singletons**: `IDbContextFactory`, all Repositories, GitService, WorkerClient, `IReleaseClient`, `UpdateCheckService`, `IPrimeScheduleApi`/`WorkerPrimeScheduleApi`, `INotesApi`/`WorkerNotesApi`, `InstallerLocator` / `WorkerLocator`, the island VMs (`ListsIslandViewModel`, `TasksIslandViewModel`, `DetailsIslandViewModel`) and `IslandsShellViewModel` (the window's DataContext)
|
||||
- **Transients**: modal VMs (`SettingsModalViewModel`, `MergeModalViewModel`, `ListSettingsModalViewModel`, `RepoImportModalViewModel`, `WeeklyReportModalViewModel`, `DiffViewerViewModel`, `WorktreesOverviewModalViewModel`, `PrimeClaudeTabViewModel`), several exposed as `Func<T>` factories for on-demand dialog creation (`Func<DiffViewerViewModel>` for the diff viewer); `ConflictResolverViewModel` via a `Func<string, ConflictResolverViewModel>` factory keyed by taskId (singleton factory, handed to `IslandsShellViewModel.ConflictResolverFactory`)
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia" Version="12.0.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.0.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.4" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.4" />
|
||||
<!-- Direct ref so the App.axaml AvaloniaEdit theme (avares://AvaloniaEdit/...) resolves at runtime. -->
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
|
||||
@@ -116,13 +116,19 @@ sealed class Program
|
||||
return new UpdateCheckService(releases, version);
|
||||
});
|
||||
|
||||
// Conflict-merge coordinator: single seam the shell wires to its resolver entry.
|
||||
sc.AddSingleton<MergeCoordinator>();
|
||||
sc.AddSingleton<IMergeCoordinator>(sp => sp.GetRequiredService<MergeCoordinator>());
|
||||
|
||||
// ViewModels
|
||||
sc.AddTransient<WorktreeModalViewModel>();
|
||||
sc.AddTransient<Func<WorktreeModalViewModel>>(sp => () => sp.GetRequiredService<WorktreeModalViewModel>());
|
||||
sc.AddTransient<DiffViewerViewModel>();
|
||||
sc.AddTransient<Func<DiffViewerViewModel>>(sp => () => sp.GetRequiredService<DiffViewerViewModel>());
|
||||
sc.AddTransient<WorktreesOverviewModalViewModel>();
|
||||
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
||||
sc.AddTransient<MergeHelperSelectionModalViewModel>();
|
||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
||||
sc.AddSingleton<IOnlineLoginService, OnlineLoginService>();
|
||||
sc.AddTransient<PrimeClaudeTabViewModel>();
|
||||
sc.AddTransient<SettingsModalViewModel>();
|
||||
sc.AddTransient<MergeModalViewModel>();
|
||||
@@ -134,29 +140,35 @@ sealed class Program
|
||||
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||
sp.GetRequiredService<WorkerClient>(), taskId));
|
||||
sp.GetRequiredService<IWorkerClient>(), taskId));
|
||||
|
||||
// Islands shell VMs
|
||||
sc.AddSingleton<ListsIslandViewModel>(sp =>
|
||||
new ListsIslandViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp,
|
||||
sp.GetRequiredService<WorkerClient>()));
|
||||
sp.GetRequiredService<IWorkerClient>()));
|
||||
sc.AddSingleton<TasksIslandViewModel>(sp =>
|
||||
new TasksIslandViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<WorkerClient>()));
|
||||
sp.GetRequiredService<IWorkerClient>()));
|
||||
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
||||
new DetailsIslandViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<WorkerClient>(),
|
||||
sp.GetRequiredService<IWorkerClient>(),
|
||||
sp,
|
||||
sp.GetRequiredService<INotesApi>()));
|
||||
sp.GetRequiredService<INotesApi>(),
|
||||
sp.GetRequiredService<IMergeCoordinator>()));
|
||||
sc.AddSingleton<MissionControlViewModel>(sp =>
|
||||
new MissionControlViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<IWorkerClient>()));
|
||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||
{
|
||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||
shell.ConflictResolverFactory =
|
||||
sp.GetRequiredService<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>();
|
||||
sp.GetRequiredService<MergeCoordinator>().Handler = shell.RequestConflictResolutionAsync;
|
||||
return shell;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public sealed class AttachmentStore
|
||||
{
|
||||
private const long MaxBytes = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
private readonly string _root;
|
||||
|
||||
public AttachmentStore(string? root = null)
|
||||
=> _root = root ?? Paths.Expand("~/.todo-app/attachments");
|
||||
|
||||
public string Root => _root;
|
||||
|
||||
public IReadOnlyList<string> EnumerateTaskIds()
|
||||
{
|
||||
if (!Directory.Exists(_root)) return Array.Empty<string>();
|
||||
return Directory.GetDirectories(_root)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(n => n is not null)
|
||||
.Select(n => n!)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public string TaskDir(string taskId)
|
||||
=> Path.Combine(_root, taskId);
|
||||
|
||||
public async Task<long> SaveAsync(string taskId, string fileName, Stream content, CancellationToken ct = default)
|
||||
{
|
||||
if (Path.GetFileName(fileName) != fileName)
|
||||
throw new ArgumentException("fileName must not contain path separators or '..'.", nameof(fileName));
|
||||
|
||||
var dir = TaskDir(taskId);
|
||||
var resolvedPath = Path.GetFullPath(Path.Combine(dir, fileName));
|
||||
|
||||
// Containment guard: resolved path must stay inside TaskDir
|
||||
var resolvedDir = Path.GetFullPath(dir);
|
||||
if (!resolvedPath.StartsWith(resolvedDir + Path.DirectorySeparatorChar, StringComparison.Ordinal)
|
||||
&& !resolvedPath.Equals(resolvedDir, StringComparison.Ordinal))
|
||||
throw new ArgumentException("fileName resolves outside the task directory.", nameof(fileName));
|
||||
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
// Buffer up to MaxBytes + 1 to detect oversize without reading fully
|
||||
await using var fs = new FileStream(resolvedPath, FileMode.Create, FileAccess.Write, FileShare.None,
|
||||
bufferSize: 81920, useAsync: true);
|
||||
|
||||
var buffer = new byte[81920];
|
||||
long total = 0;
|
||||
int read;
|
||||
while ((read = await content.ReadAsync(buffer, ct)) > 0)
|
||||
{
|
||||
total += read;
|
||||
if (total > MaxBytes)
|
||||
{
|
||||
fs.Close();
|
||||
try { File.Delete(resolvedPath); } catch { }
|
||||
throw new InvalidOperationException($"Attachment exceeds the 5 MB size limit.");
|
||||
}
|
||||
await fs.WriteAsync(buffer.AsMemory(0, read), ct);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
public void DeleteFile(string taskId, string fileName)
|
||||
{
|
||||
if (Path.GetFileName(fileName) != fileName)
|
||||
return; // traversal attempt — ignore silently
|
||||
|
||||
var dir = TaskDir(taskId);
|
||||
var resolvedPath = Path.GetFullPath(Path.Combine(dir, fileName));
|
||||
var resolvedDir = Path.GetFullPath(dir);
|
||||
if (!resolvedPath.StartsWith(resolvedDir + Path.DirectorySeparatorChar, StringComparison.Ordinal)
|
||||
&& !resolvedPath.Equals(resolvedDir, StringComparison.Ordinal))
|
||||
return; // containment violation — ignore silently
|
||||
|
||||
try { File.Delete(resolvedPath); } catch (DirectoryNotFoundException) { } catch (FileNotFoundException) { }
|
||||
}
|
||||
|
||||
public void DeleteTaskDir(string taskId)
|
||||
{
|
||||
var dir = TaskDir(taskId);
|
||||
try { Directory.Delete(dir, recursive: true); } catch (DirectoryNotFoundException) { } catch (IOException) { }
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,18 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
|
||||
## 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.
|
||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
||||
- **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. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||
- **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)
|
||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
||||
- **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`
|
||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||
- **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)
|
||||
- **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) and `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`)
|
||||
- **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)` always returns a usable row. Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||
- **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
|
||||
|
||||
## Repositories
|
||||
@@ -25,6 +28,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
||||
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
||||
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
||||
- **TaskAttachmentRepository** — `AddAsync`, `UpdateAsync`, `GetAsync(taskId, fileName)`, `ListByTaskIdAsync`, `DeleteAsync(taskId, fileName)`, `DeleteAllForTaskAsync`
|
||||
|
||||
## Infrastructure
|
||||
|
||||
@@ -32,14 +36,15 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
||||
- **IDbContextFactory<ClaudeDoDbContext>** — registered in DI; used by singleton consumers (e.g. Worker hosted service)
|
||||
- **Paths** — expands `~` and `%USERPROFILE%`, resolves relative paths. App root: `~/.todo-app`
|
||||
- **AppSettings** — loads `~/.todo-app/ui.config.json` (DbPath, SignalRUrl)
|
||||
- **AttachmentStore** — dependency-free file store; default root `~/.todo-app/attachments/<taskId>/`. `SaveAsync` enforces a 5 MB cap and path-traversal/containment guard. Also exposes `DeleteFile`, `DeleteTaskDir`, `TaskDir`, `Root`, and `EnumerateTaskIds` (used by the worker orphan sweep). Attachment files live outside git worktrees intentionally.
|
||||
|
||||
## Git
|
||||
|
||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Operations: worktree add/remove, add all, commit (stdin for message), merge ff-only, rev-parse, diff-stat, has-changes, is-git-repo, `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`
|
||||
- **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
|
||||
|
||||
## Schema
|
||||
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`. 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).
|
||||
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`. `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
|
||||
|
||||
|
||||
@@ -46,10 +46,12 @@ public class ClaudeDoDbContext : DbContext
|
||||
public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
|
||||
public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
|
||||
public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
|
||||
public DbSet<TaskAttachmentEntity> TaskAttachments => Set<TaskAttachmentEntity>();
|
||||
public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>();
|
||||
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
|
||||
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
|
||||
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
|
||||
public DbSet<SessionSkillEntity> SessionSkills => Set<SessionSkillEntity>();
|
||||
|
||||
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
|
||||
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
||||
|
||||
@@ -44,6 +44,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.DailyPrepMaxTasks)
|
||||
.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.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
||||
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
||||
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
||||
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.IsManual).HasColumnName("is_manual").IsRequired().HasDefaultValue(false);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ClaudeDo.Data.Configuration;
|
||||
|
||||
public class TaskAttachmentEntityConfiguration : IEntityTypeConfiguration<TaskAttachmentEntity>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TaskAttachmentEntity> builder)
|
||||
{
|
||||
builder.ToTable("task_attachments");
|
||||
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Id).HasColumnName("id");
|
||||
builder.Property(a => a.TaskId).HasColumnName("task_id").IsRequired();
|
||||
builder.Property(a => a.FileName).HasColumnName("file_name").IsRequired();
|
||||
builder.Property(a => a.ByteSize).HasColumnName("byte_size").IsRequired();
|
||||
builder.Property(a => a.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||
|
||||
builder.HasOne(a => a.Task)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.TaskId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(a => a.TaskId).HasDatabaseName("idx_task_attachments_task_id");
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,10 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||
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.IsManual).HasColumnName("is_manual").HasDefaultValue(false);
|
||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||
|
||||
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
||||
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace ClaudeDo.Data.Git;
|
||||
|
||||
/// <summary>
|
||||
/// One piece of a conflicted file: either common ("stable") text both sides agree on,
|
||||
/// or a conflict region holding the two — or, with diff3 markers, three — competing versions.
|
||||
/// </summary>
|
||||
public sealed record MergeSegment
|
||||
{
|
||||
public bool IsConflict { get; init; }
|
||||
|
||||
/// <summary>Stable text (verbatim, line endings preserved) when <see cref="IsConflict"/> is false.</summary>
|
||||
public string Text { get; init; } = "";
|
||||
|
||||
/// <summary>"Ours" side (the target branch) when <see cref="IsConflict"/> is true.</summary>
|
||||
public string Ours { get; init; } = "";
|
||||
|
||||
/// <summary>Merge base, present only when the merge used diff3 conflict style; null otherwise.</summary>
|
||||
public string? Base { get; init; }
|
||||
|
||||
/// <summary>"Theirs" side (the incoming branch) when <see cref="IsConflict"/> is true.</summary>
|
||||
public string Theirs { get; init; } = "";
|
||||
|
||||
public static MergeSegment Stable(string text) => new() { Text = text };
|
||||
|
||||
public static MergeSegment Conflict(string ours, string? @base, string theirs) =>
|
||||
new() { IsConflict = true, Ours = ours, Base = @base, Theirs = theirs };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a conflicted file's text into ordered stable / conflict segments and reassembles it.
|
||||
/// Reads git conflict markers verbatim, so a file with no markers yields a single stable
|
||||
/// segment, and reassembling the stable text plus one chosen resolution per conflict
|
||||
/// round-trips the file exactly (line endings included).
|
||||
/// </summary>
|
||||
public static class ConflictMarkerParser
|
||||
{
|
||||
private const string OursMarker = "<<<<<<<";
|
||||
private const string BaseMarker = "|||||||";
|
||||
private const string SepMarker = "=======";
|
||||
private const string TheirsMarker = ">>>>>>>";
|
||||
|
||||
public static IReadOnlyList<MergeSegment> Parse(string fileText)
|
||||
{
|
||||
var segments = new List<MergeSegment>();
|
||||
var lines = SplitKeepLineEndings(fileText);
|
||||
var stable = new StringBuilder();
|
||||
var i = 0;
|
||||
|
||||
while (i < lines.Count)
|
||||
{
|
||||
if (!IsMarker(lines[i], OursMarker))
|
||||
{
|
||||
stable.Append(lines[i++]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stable.Length > 0)
|
||||
{
|
||||
segments.Add(MergeSegment.Stable(stable.ToString()));
|
||||
stable.Clear();
|
||||
}
|
||||
|
||||
i++; // consume "<<<<<<<"
|
||||
var ours = new StringBuilder();
|
||||
while (i < lines.Count && !IsMarker(lines[i], BaseMarker) && !IsMarker(lines[i], SepMarker))
|
||||
ours.Append(lines[i++]);
|
||||
|
||||
string? @base = null;
|
||||
if (i < lines.Count && IsMarker(lines[i], BaseMarker))
|
||||
{
|
||||
i++; // consume "|||||||"
|
||||
var baseText = new StringBuilder();
|
||||
while (i < lines.Count && !IsMarker(lines[i], SepMarker))
|
||||
baseText.Append(lines[i++]);
|
||||
@base = baseText.ToString();
|
||||
}
|
||||
|
||||
if (i < lines.Count && IsMarker(lines[i], SepMarker)) i++; // consume "======="
|
||||
|
||||
var theirs = new StringBuilder();
|
||||
while (i < lines.Count && !IsMarker(lines[i], TheirsMarker))
|
||||
theirs.Append(lines[i++]);
|
||||
|
||||
if (i < lines.Count && IsMarker(lines[i], TheirsMarker)) i++; // consume ">>>>>>>"
|
||||
|
||||
segments.Add(MergeSegment.Conflict(ours.ToString(), @base, theirs.ToString()));
|
||||
}
|
||||
|
||||
if (stable.Length > 0)
|
||||
segments.Add(MergeSegment.Stable(stable.ToString()));
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/// <summary>True when the file still contains an opening conflict marker.</summary>
|
||||
public static bool HasConflicts(string fileText) =>
|
||||
SplitKeepLineEndings(fileText).Any(l => IsMarker(l, OursMarker));
|
||||
|
||||
/// <summary>
|
||||
/// Reassembles a file from its segments. Stable segments emit their text verbatim;
|
||||
/// each conflict segment emits whatever <paramref name="resolveConflict"/> returns for it.
|
||||
/// </summary>
|
||||
public static string Compose(
|
||||
IEnumerable<MergeSegment> segments, Func<MergeSegment, string> resolveConflict) =>
|
||||
string.Concat(segments.Select(s => s.IsConflict ? resolveConflict(s) : s.Text));
|
||||
|
||||
// A marker line starts with exactly the 7-char marker, then end-of-line or whitespace/label.
|
||||
private static bool IsMarker(string line, string marker)
|
||||
{
|
||||
if (!line.StartsWith(marker, StringComparison.Ordinal)) return false;
|
||||
if (line.Length == marker.Length) return true;
|
||||
return line[marker.Length] is ' ' or '\t' or '\r' or '\n';
|
||||
}
|
||||
|
||||
// Splits into physical lines, each retaining its trailing "\n" (and "\r" if present).
|
||||
private static List<string> SplitKeepLineEndings(string s)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
var i = 0;
|
||||
while (i < s.Length)
|
||||
{
|
||||
var nl = s.IndexOf('\n', i);
|
||||
if (nl < 0) { lines.Add(s[i..]); break; }
|
||||
lines.Add(s[i..(nl + 1)]);
|
||||
i = nl + 1;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -71,9 +71,22 @@ public sealed class GitService
|
||||
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)
|
||||
throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
|
||||
return !string.IsNullOrWhiteSpace(stdout);
|
||||
@@ -252,8 +265,11 @@ public sealed class GitService
|
||||
public async Task<(int ExitCode, string Stderr)> MergeNoFfAsync(
|
||||
string repoDir, string sourceBranch, string message, CancellationToken ct = default)
|
||||
{
|
||||
// diff3 conflict style writes the merge base (|||||||) into conflict markers so the
|
||||
// in-app resolver can show a true three-way view. It only enriches conflicted hunks;
|
||||
// clean merges are unaffected.
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||
["merge", "--no-ff", "-m", message, sourceBranch], ct);
|
||||
["-c", "merge.conflictStyle=diff3", "merge", "--no-ff", "-m", message, sourceBranch], ct);
|
||||
return (exitCode, stderr);
|
||||
}
|
||||
|
||||
@@ -277,17 +293,6 @@ public sealed class GitService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a conflicted file's blob at a merge stage: 1=base, 2=ours, 3=theirs.
|
||||
/// Returns null when the stage doesn't exist (e.g. add/add conflict has no base).
|
||||
/// Output is NOT trimmed so file content round-trips exactly.
|
||||
/// </summary>
|
||||
public async Task<string?> ShowStageAsync(string repoDir, int stage, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $":{stage}:{path}"], ct, trimOutput: false);
|
||||
return exitCode == 0 ? stdout : null;
|
||||
}
|
||||
|
||||
public async Task AddPathAsync(string repoDir, string path, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["add", "--", path], ct);
|
||||
@@ -338,13 +343,6 @@ public sealed class GitService
|
||||
.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(
|
||||
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,739 @@
|
||||
// <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("20260622150934_AddTaskAttachments")]
|
||||
partial class AddTaskAttachments
|
||||
{
|
||||
/// <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<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>("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.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<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,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTaskAttachments : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "task_attachments",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
task_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
file_name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
byte_size = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_task_attachments", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_task_attachments_tasks_task_id",
|
||||
column: x => x.task_id,
|
||||
principalTable: "tasks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "idx_task_attachments_task_id",
|
||||
table: "task_attachments",
|
||||
column: "task_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "task_attachments");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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");
|
||||
@@ -74,6 +78,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("report_excluded_paths");
|
||||
|
||||
b.Property<string>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<int>("StandupWeekday")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
@@ -168,6 +176,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
@@ -194,6 +206,12 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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")
|
||||
@@ -256,6 +274,41 @@ namespace ClaudeDo.Data.Migrations
|
||||
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")
|
||||
@@ -294,6 +347,38 @@ namespace ClaudeDo.Data.Migrations
|
||||
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")
|
||||
@@ -331,6 +416,12 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
b.Property<bool>("IsManual")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_manual");
|
||||
|
||||
b.Property<bool>("IsMyDay")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
@@ -405,6 +496,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("scheduled_for");
|
||||
|
||||
b.Property<string>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
@@ -625,6 +720,17 @@ namespace ClaudeDo.Data.Migrations
|
||||
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)
|
||||
|
||||
@@ -27,4 +27,11 @@ public sealed class AppSettingsEntity
|
||||
|
||||
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -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,7 @@ public sealed class ListConfigEntity
|
||||
public string? SystemPrompt { get; set; }
|
||||
public string? AgentPath { get; set; }
|
||||
public int? MaxTurns { get; set; }
|
||||
public string? SessionSkills { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public ListEntity List { get; set; } = null!;
|
||||
|
||||
@@ -8,6 +8,9 @@ public sealed class ListEntity
|
||||
public string? WorkingDir { get; set; }
|
||||
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
||||
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
|
||||
public ListConfigEntity? Config { get; set; }
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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.</summary>
|
||||
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model)
|
||||
{
|
||||
var alias = (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, 30);
|
||||
}
|
||||
|
||||
public static ModelPreset For(string? json, string? model) => For(Parse(json), model);
|
||||
|
||||
// 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,11 +2,28 @@ namespace ClaudeDo.Data.Models;
|
||||
|
||||
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>
|
||||
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
||||
|
||||
public const string DefaultAlias = "sonnet";
|
||||
public const string PlanningAlias = "opus";
|
||||
|
||||
public const string ListDefaultSentinel = "(default)";
|
||||
public const string TaskInheritSentinel = "(inherit)";
|
||||
|
||||
/// <summary>
|
||||
/// Validate a model alias from external input. Null/blank → null (inherit).
|
||||
/// Returns the canonical lowercase alias; throws on an unknown value.
|
||||
/// </summary>
|
||||
public static string? NormalizeAlias(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;
|
||||
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
public sealed class TaskAttachmentEntity
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TaskId { get; init; }
|
||||
public required string FileName { get; set; }
|
||||
public long ByteSize { get; set; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
|
||||
// Navigation property
|
||||
public TaskEntity Task { get; set; } = null!;
|
||||
}
|
||||
@@ -43,8 +43,13 @@ public sealed class TaskEntity
|
||||
public int? MaxTurns { get; set; }
|
||||
public bool IsStarred { 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 int SortOrder { get; set; }
|
||||
public string? SessionSkills { get; set; }
|
||||
|
||||
public string? ParentTaskId { get; set; }
|
||||
public string? PlanningSessionId { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Text;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -18,6 +18,8 @@ public static class PromptFiles
|
||||
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||
PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.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))
|
||||
};
|
||||
|
||||
@@ -63,6 +65,8 @@ public static class PromptFiles
|
||||
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||
PromptKind.ImprovementChild => ImprovementChildDefault,
|
||||
PromptKind.Refine => RefineDefault,
|
||||
PromptKind.MergeHelper => MergeHelperDefault,
|
||||
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
|
||||
_ => ""
|
||||
};
|
||||
|
||||
@@ -82,7 +86,10 @@ public static class PromptFiles
|
||||
## Out-of-scope improvements
|
||||
If you notice worthwhile work that is genuinely outside this task's scope
|
||||
(a refactor, a follow-up, tech debt), do NOT do it here. File it with
|
||||
SuggestImprovement(title, description) and stay focused on the task at hand.
|
||||
SuggestImprovement(title, description, model) and stay focused on the task at hand.
|
||||
Set `model` to the cheapest model that can do the follow-up well — 'haiku' for
|
||||
trivial/mechanical work, 'sonnet' for normal coding, 'opus' only for genuinely
|
||||
complex work (cheapest to most capable: haiku < sonnet < opus).
|
||||
|
||||
## Working in the repo
|
||||
- Read a file before editing it. Match the conventions already in this codebase —
|
||||
@@ -102,9 +109,12 @@ public static class PromptFiles
|
||||
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||
|
||||
## You are running unattended
|
||||
You run autonomously with no human watching. There is no one to answer mid-task
|
||||
questions, so never stop to ask — make the most reasonable decision, note the
|
||||
assumption, and continue.
|
||||
You run autonomously, usually with no one watching. Default to making the most
|
||||
reasonable decision yourself, noting the assumption, and continuing — do not stop
|
||||
for routine choices. The one exception: at a genuine fork where a wrong guess
|
||||
would be costly or hard to undo (an irreversible action, contradictory
|
||||
requirements), you may call AskUser(question) to ask the user and wait briefly for
|
||||
an answer. If no one responds in time, proceed on your best judgment.
|
||||
|
||||
## When you are blocked
|
||||
If something genuinely prevents you from completing part of the task (missing
|
||||
@@ -122,8 +132,8 @@ public static class PromptFiles
|
||||
# Out-of-scope follow-up
|
||||
|
||||
You are an improvement follow-up that another task filed via SuggestImprovement.
|
||||
It was deliberately scoped narrow. Do EXACTLY what this task's title and
|
||||
description ask — nothing more.
|
||||
It was deliberately scoped narrow, and is intentionally a small, cheap unit of
|
||||
work. Do EXACTLY what this task's title and description ask — nothing more.
|
||||
|
||||
- Make the smallest change that satisfies the task. No opportunistic refactors,
|
||||
renames, reformatting, or "while I'm here" cleanup beyond what is asked.
|
||||
@@ -150,6 +160,14 @@ public static class PromptFiles
|
||||
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||
done-state, ordered so dependencies come first.
|
||||
|
||||
For each subtask, pass CreateChildTask's `model` argument set to the CHEAPEST
|
||||
model that can do that subtask well. Models, cheapest to most capable:
|
||||
haiku < sonnet < opus.
|
||||
- haiku — trivial/mechanical work: doc tweaks, simple renames, small localized edits.
|
||||
- sonnet — normal coding work; the sensible default when unsure.
|
||||
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
||||
Do not default everything to opus — most subtasks are haiku or sonnet.
|
||||
""";
|
||||
|
||||
private const string PlanningInitialDefault = """
|
||||
@@ -210,6 +228,89 @@ public static class PromptFiles
|
||||
task, stop.
|
||||
""";
|
||||
|
||||
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.
|
||||
""";
|
||||
|
||||
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 = """
|
||||
You are generating a concise weekly standup report for a software developer,
|
||||
covering {start} to {end}.
|
||||
|
||||
@@ -64,6 +64,7 @@ public sealed class AppSettingsRepository
|
||||
? null : updated.ReportExcludedPaths;
|
||||
row.StandupWeekday = updated.StandupWeekday;
|
||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,13 @@ namespace ClaudeDo.Data.Repositories;
|
||||
public sealed class ListRepository
|
||||
{
|
||||
private readonly ClaudeDoDbContext _context;
|
||||
private readonly AttachmentStore _attachments;
|
||||
|
||||
public ListRepository(ClaudeDoDbContext context) => _context = context;
|
||||
public ListRepository(ClaudeDoDbContext context, AttachmentStore? attachments = null)
|
||||
{
|
||||
_context = context;
|
||||
_attachments = attachments ?? new AttachmentStore();
|
||||
}
|
||||
|
||||
public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
|
||||
{
|
||||
@@ -23,7 +28,13 @@ public sealed class ListRepository
|
||||
|
||||
public async Task DeleteAsync(string listId, CancellationToken ct = default)
|
||||
{
|
||||
var taskIds = await _context.Tasks
|
||||
.Where(t => t.ListId == listId)
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(ct);
|
||||
await _context.Lists.Where(l => l.Id == listId).ExecuteDeleteAsync(ct);
|
||||
foreach (var id in taskIds)
|
||||
_attachments.DeleteTaskDir(id);
|
||||
}
|
||||
|
||||
public async Task<ListEntity?> GetByIdAsync(string listId, CancellationToken ct = default)
|
||||
@@ -66,6 +77,7 @@ public sealed class ListRepository
|
||||
existing.SystemPrompt = config.SystemPrompt;
|
||||
existing.AgentPath = config.AgentPath;
|
||||
existing.MaxTurns = config.MaxTurns;
|
||||
existing.SessionSkills = config.SessionSkills;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Data.Repositories;
|
||||
|
||||
public sealed class TaskAttachmentRepository
|
||||
{
|
||||
private readonly ClaudeDoDbContext _context;
|
||||
|
||||
public TaskAttachmentRepository(ClaudeDoDbContext context) => _context = context;
|
||||
|
||||
public async Task AddAsync(TaskAttachmentEntity entity, CancellationToken ct = default)
|
||||
{
|
||||
_context.TaskAttachments.Add(entity);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<List<TaskAttachmentEntity>> ListByTaskIdAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.TaskAttachments
|
||||
.Where(a => a.TaskId == taskId)
|
||||
.OrderBy(a => a.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<TaskAttachmentEntity?> GetAsync(string taskId, string fileName, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.TaskAttachments
|
||||
.FirstOrDefaultAsync(a => a.TaskId == taskId && a.FileName == fileName, ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaskAttachmentEntity entity, CancellationToken ct = default)
|
||||
{
|
||||
_context.TaskAttachments.Update(entity);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string taskId, string fileName, CancellationToken ct = default)
|
||||
{
|
||||
await _context.TaskAttachments
|
||||
.Where(a => a.TaskId == taskId && a.FileName == fileName)
|
||||
.ExecuteDeleteAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAllForTaskAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
await _context.TaskAttachments
|
||||
.Where(a => a.TaskId == taskId)
|
||||
.ExecuteDeleteAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,13 @@ namespace ClaudeDo.Data.Repositories;
|
||||
public sealed class TaskRepository
|
||||
{
|
||||
private readonly ClaudeDoDbContext _context;
|
||||
private readonly AttachmentStore _attachments;
|
||||
|
||||
public TaskRepository(ClaudeDoDbContext context) => _context = context;
|
||||
public TaskRepository(ClaudeDoDbContext context, AttachmentStore? attachments = null)
|
||||
{
|
||||
_context = context;
|
||||
_attachments = attachments ?? new AttachmentStore();
|
||||
}
|
||||
|
||||
#region CRUD
|
||||
|
||||
@@ -37,6 +42,7 @@ public sealed class TaskRepository
|
||||
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
await _context.Tasks.Where(t => t.Id == taskId).ExecuteDeleteAsync(ct);
|
||||
_attachments.DeleteTaskDir(taskId);
|
||||
}
|
||||
|
||||
public async Task<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)
|
||||
@@ -87,6 +93,22 @@ public sealed class TaskRepository
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all tasks that qualify as "real" Idle backlog items for online mirroring:
|
||||
/// Status==Idle, no parent, PlanningPhase==None, not blocked.
|
||||
/// </summary>
|
||||
public async Task<List<TaskEntity>> GetAllIdleBacklogAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Tasks
|
||||
.AsNoTracking()
|
||||
.Where(t => t.Status == TaskStatus.Idle
|
||||
&& t.ParentTaskId == null
|
||||
&& t.PlanningPhase == PlanningPhase.None
|
||||
&& t.BlockedByTaskId == null)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Status transitions
|
||||
@@ -167,6 +189,7 @@ public sealed class TaskRepository
|
||||
string? systemPrompt,
|
||||
string? agentPath,
|
||||
int? maxTurns = null,
|
||||
string? sessionSkills = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _context.Tasks
|
||||
@@ -175,7 +198,8 @@ public sealed class TaskRepository
|
||||
.SetProperty(t => t.Model, model)
|
||||
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
||||
.SetProperty(t => t.AgentPath, agentPath)
|
||||
.SetProperty(t => t.MaxTurns, maxTurns), ct);
|
||||
.SetProperty(t => t.MaxTurns, maxTurns)
|
||||
.SetProperty(t => t.SessionSkills, sessionSkills), ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -197,6 +221,7 @@ public sealed class TaskRepository
|
||||
string? description,
|
||||
string? commitType,
|
||||
string? createdBy = null,
|
||||
string? model = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
||||
@@ -223,6 +248,7 @@ public sealed class TaskRepository
|
||||
ParentTaskId = parentId,
|
||||
SortOrder = (maxSort ?? -1) + 1,
|
||||
CreatedBy = createdBy,
|
||||
Model = ModelRegistry.NormalizeAlias(model),
|
||||
};
|
||||
_context.Tasks.Add(child);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace ClaudeDo.Data;
|
||||
/// </summary>
|
||||
public static class TaskPromptComposer
|
||||
{
|
||||
public static string Compose(string title, string? description, IEnumerable<(string Title, bool Completed)> subtasks)
|
||||
public static string Compose(string title, string? description, IEnumerable<(string Title, bool Completed)> subtasks,
|
||||
IEnumerable<string>? attachmentPaths = null)
|
||||
{
|
||||
var sb = new StringBuilder((title ?? "").Trim());
|
||||
|
||||
@@ -24,6 +25,14 @@ public static class TaskPromptComposer
|
||||
sb.Append("- [ ] ").Append(s.Title).Append('\n');
|
||||
}
|
||||
|
||||
var paths = attachmentPaths?.ToList();
|
||||
if (paths is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n## Reference files\nThese files were attached to this task as read-only reference (they live outside the repo). Read them as needed:\n");
|
||||
foreach (var p in paths)
|
||||
sb.Append("- ").Append(p).Append('\n');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,104 +38,6 @@ public partial class App : Application
|
||||
var localizer = new Localizer(localeStore, initialLang);
|
||||
TrExtension.Localizer = localizer;
|
||||
|
||||
// --- Self-update pre-flight ---
|
||||
// Resolve current exe path. Assembly.Location may point to a .dll for apphost-based
|
||||
// .NET apps; swap to the .exe companion when that happens.
|
||||
var currentExePath = Assembly.GetEntryAssembly()!.Location;
|
||||
if (currentExePath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
currentExePath = System.IO.Path.ChangeExtension(currentExePath, ".exe");
|
||||
}
|
||||
|
||||
// Arg form: --replace-self "<old-path>"
|
||||
var replaceSelfIndex = Array.FindIndex(e.Args, a => a.Equals("--replace-self", StringComparison.OrdinalIgnoreCase));
|
||||
if (replaceSelfIndex >= 0 && replaceSelfIndex + 1 < e.Args.Length)
|
||||
{
|
||||
var oldPath = e.Args[replaceSelfIndex + 1];
|
||||
var relaunched = await SelfUpdater.HandleReplaceSelfAsync(
|
||||
oldPath: oldPath,
|
||||
currentExePath: currentExePath,
|
||||
launchProcess: path =>
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
});
|
||||
if (relaunched)
|
||||
{
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
// Replacement failed — fall through to normal wizard from the temp location.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal launch: check for a newer installer.
|
||||
using var selfUpdateHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
var selfUpdateReleases = new ReleaseClient(selfUpdateHttp);
|
||||
var currentVersion = GetInstallerVersion();
|
||||
|
||||
var decision = await SelfUpdater.DecideUpdateAsync(selfUpdateReleases, currentVersion, CancellationToken.None);
|
||||
if (decision.Kind == SelfUpdateDecisionKind.UpdateAvailable)
|
||||
{
|
||||
var prompt = new SelfUpdatePromptWindow(currentVersion, decision.LatestVersion!);
|
||||
DarkTitleBar.Apply(prompt);
|
||||
var ok = prompt.ShowDialog() == true;
|
||||
if (!ok)
|
||||
{
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
if (prompt.Choice == SelfUpdateChoice.Update)
|
||||
{
|
||||
prompt.ShowProgress("Downloading...");
|
||||
var tempDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ClaudeDo.Installer.Update");
|
||||
var verifiedPath = await SelfUpdater.DownloadAndVerifyAsync(
|
||||
selfUpdateReleases,
|
||||
decision.InstallerAsset!,
|
||||
decision.ChecksumsAsset!,
|
||||
tempDir,
|
||||
new Progress<long>(_ => { }),
|
||||
CancellationToken.None);
|
||||
|
||||
if (verifiedPath is null)
|
||||
{
|
||||
MessageBox.Show(prompt,
|
||||
"Update download or verification failed. Continuing with current installer.",
|
||||
"ClaudeDo Installer", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo(verifiedPath)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
};
|
||||
psi.ArgumentList.Add("--replace-self");
|
||||
psi.ArgumentList.Add(currentExePath);
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(prompt,
|
||||
"Failed to launch updated installer: " + ex.Message + "\nContinuing with current installer.",
|
||||
"ClaudeDo Installer", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
// SelfUpdateChoice.Continue — fall through to normal wizard.
|
||||
}
|
||||
// No-update or check failed — fall through to normal wizard.
|
||||
}
|
||||
|
||||
// --- Existing wizard start-up unchanged below this line ---
|
||||
|
||||
_services = BuildServices(localizer);
|
||||
|
||||
var context = _services.GetRequiredService<InstallContext>();
|
||||
|
||||
@@ -12,14 +12,22 @@ Note: this is the one project where `System.Windows` is correct (WPF, not Avalon
|
||||
- Entry point: `App.xaml` / `App.xaml.cs` (no `Program.cs`)
|
||||
- References: `ClaudeDo.Data`, `ClaudeDo.Releases`, `ClaudeDo.Localization`
|
||||
- Manifests: `app.manifest` (requireAdministrator, Release) / `app.debug.manifest` (asInvoker, Debug)
|
||||
- Only CLI arg: `--replace-self <old-path>` (self-update handoff)
|
||||
- No CLI args — mode is detected from `install.json` + the Gitea API
|
||||
|
||||
## Startup Sequence (`App.OnStartup`)
|
||||
|
||||
1. Load locale
|
||||
2. Self-update preflight — `SelfUpdater.DecideUpdateAsync` checks Gitea API; if a newer installer exists, download + checksum verify + relaunch with `--replace-self <old-path>`
|
||||
3. Detect mode — `InstallModeDetector` reads `install.json` + Gitea API
|
||||
4. Open `WizardWindow` (FreshInstall / Update) or `SettingsWindow` (Config)
|
||||
2. Detect mode — `InstallModeDetector` reads `install.json` + Gitea API
|
||||
3. Open `WizardWindow` (FreshInstall / Update) or `SettingsWindow` (Config)
|
||||
|
||||
The installer does **not** self-update. Each release ships a stable-named
|
||||
`ClaudeDo.Installer.exe` asset (permanent URL
|
||||
`…/releases/latest/download/ClaudeDo.Installer.exe`); the installer never checks for or
|
||||
replaces itself on launch. The in-app "Update" button relaunches the on-disk installer to
|
||||
run the app update — the installer binary itself only changes when the user downloads a
|
||||
fresh copy. App-update detection is unaffected: `WriteInstallManifestStep` records
|
||||
`ctx.InstalledVersion` (the release tag from `DownloadAndExtractStep`), which
|
||||
`InstallModeDetector` compares against the latest tag.
|
||||
|
||||
## Modes (`Core/InstallerMode.cs`)
|
||||
|
||||
@@ -56,8 +64,7 @@ Installer/
|
||||
Interfaces/ — IInstallStep + StepResult/StepStatus/StepProgress, IInstallerPage
|
||||
Pages/ — WelcomePage, PathsPage, ServicePage, UiSettingsPage, InstallPage
|
||||
(each: ViewModel + View.xaml)
|
||||
Views/ — WizardWindow(+WizardViewModel), SettingsWindow(+SettingsViewModel),
|
||||
SelfUpdatePromptWindow
|
||||
Views/ — WizardWindow(+WizardViewModel), SettingsWindow(+SettingsViewModel)
|
||||
```
|
||||
|
||||
## Key Step Behaviors
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Releases;
|
||||
|
||||
@@ -77,10 +78,23 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
||||
var appBak = appDest + ".bak";
|
||||
var workerBak = workerDest + ".bak";
|
||||
|
||||
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
||||
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
||||
if (Directory.Exists(appDest)) Directory.Move(appDest, appBak);
|
||||
if (Directory.Exists(workerDest)) Directory.Move(workerDest, workerBak);
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||
if (Directory.Exists(appDest)) MoveWithRetry(appDest, appBak);
|
||||
if (Directory.Exists(workerDest)) MoveWithRetry(workerDest, workerBak);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// 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 the existing app/worker files — they are 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...");
|
||||
Directory.CreateDirectory(ctx.InstallDirectory);
|
||||
@@ -91,17 +105,17 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Roll back to previous binaries.
|
||||
if (Directory.Exists(appDest)) Directory.Delete(appDest, recursive: true);
|
||||
if (Directory.Exists(workerDest)) Directory.Delete(workerDest, recursive: true);
|
||||
if (Directory.Exists(appBak)) Directory.Move(appBak, appDest);
|
||||
if (Directory.Exists(workerBak)) Directory.Move(workerBak, workerDest);
|
||||
if (Directory.Exists(appDest)) DeleteWithRetry(appDest);
|
||||
if (Directory.Exists(workerDest)) DeleteWithRetry(workerDest);
|
||||
if (Directory.Exists(appBak)) MoveWithRetry(appBak, appDest);
|
||||
if (Directory.Exists(workerBak)) MoveWithRetry(workerBak, workerDest);
|
||||
return StepResult.Fail(
|
||||
$"Extraction failed; previous binaries have been restored: {ex.Message}.");
|
||||
}
|
||||
|
||||
// Success — drop stash.
|
||||
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
||||
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
||||
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||
|
||||
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
||||
return StepResult.Ok();
|
||||
@@ -111,4 +125,26 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
||||
try { Directory.Delete(scratchDir, recursive: true); } 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)...");
|
||||
var installDir = ctx.InstallDirectory;
|
||||
var killedAny = false;
|
||||
foreach (var name in ProcessNames)
|
||||
{
|
||||
foreach (var p in Process.GetProcessesByName(name))
|
||||
{
|
||||
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;
|
||||
p.Kill(entireProcessTree: true);
|
||||
p.WaitForExit(10000);
|
||||
killedAny = true;
|
||||
}
|
||||
catch { /* process may have exited or be inaccessible */ }
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ public sealed class WriteUninstallRegistryStep : IInstallStep
|
||||
// the single-file temp extract is gone once this process exits.
|
||||
var sourceExe = Environment.ProcessPath
|
||||
?? throw new InvalidOperationException("Cannot resolve running installer path.");
|
||||
// In the self-update path the installer already runs from uninstaller/ (the
|
||||
// --replace-self handoff put it there), so source == target and the copy would
|
||||
// throw. Skip it; the binary is already in place.
|
||||
// When relaunched from the installed copy (e.g. the Apps & Features "Rerun
|
||||
// Installer" entry points at uninstaller/ClaudeDo.Installer.exe), source == target
|
||||
// and the copy would throw. Skip it; the binary is already in place.
|
||||
var alreadyInPlace = string.Equals(
|
||||
Path.GetFullPath(sourceExe), Path.GetFullPath(targetExe), StringComparison.OrdinalIgnoreCase);
|
||||
if (!alreadyInPlace)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<Window x:Class="ClaudeDo.Installer.Views.SelfUpdatePromptWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
|
||||
Title="ClaudeDo Installer Update"
|
||||
Width="460" Height="200"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
Background="#1a1a1a" Foreground="#f0f0f0">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="16" FontWeight="SemiBold" Text="{loc:Tr installer.selfUpdate.heading}"/>
|
||||
<TextBlock Grid.Row="1" Margin="0,8,0,0" TextWrapping="Wrap" x:Name="DetailText"/>
|
||||
<TextBlock Grid.Row="2" Margin="0,12,0,0" TextWrapping="Wrap" Foreground="#a0a0a0" x:Name="ProgressText" Visibility="Collapsed"/>
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="UpdateBtn" Content="{loc:Tr installer.selfUpdate.update}" MinWidth="90" Margin="4,0" Padding="10,4" Click="UpdateBtn_Click" IsDefault="True"/>
|
||||
<Button x:Name="ContinueBtn" Content="{loc:Tr installer.selfUpdate.continueAnyway}" MinWidth="140" Margin="4,0" Padding="10,4" Click="ContinueBtn_Click"/>
|
||||
<Button x:Name="CancelBtn" Content="{loc:Tr installer.nav.cancel}" MinWidth="90" Margin="4,0" Padding="10,4" Click="CancelBtn_Click" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace ClaudeDo.Installer.Views;
|
||||
|
||||
public enum SelfUpdateChoice { Update, Continue, Cancel }
|
||||
|
||||
public partial class SelfUpdatePromptWindow : Window
|
||||
{
|
||||
public SelfUpdateChoice Choice { get; private set; } = SelfUpdateChoice.Cancel;
|
||||
|
||||
public SelfUpdatePromptWindow(string currentVersion, string latestVersion)
|
||||
{
|
||||
InitializeComponent();
|
||||
DetailText.Text = $"Installer v{latestVersion} is available (you are running v{currentVersion}). Update before continuing?";
|
||||
}
|
||||
|
||||
public void ShowProgress(string text)
|
||||
{
|
||||
ProgressText.Text = text;
|
||||
ProgressText.Visibility = Visibility.Visible;
|
||||
UpdateBtn.IsEnabled = false;
|
||||
ContinueBtn.IsEnabled = false;
|
||||
}
|
||||
|
||||
private void UpdateBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Choice = SelfUpdateChoice.Update;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void ContinueBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Choice = SelfUpdateChoice.Continue;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void CancelBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Choice = SelfUpdateChoice.Cancel;
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,15 @@
|
||||
"tabWorktrees": "Worktrees",
|
||||
"tabFiles": "Dateien",
|
||||
"tabPrime": "Prime Claude",
|
||||
"tabSkills": "Skills",
|
||||
"general": {
|
||||
"defaultInstructions": "Standard-Anweisungen",
|
||||
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
||||
"model": "Modell",
|
||||
"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",
|
||||
"maxParallelExecutions": "Max. parallele Ausführungen",
|
||||
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
||||
@@ -25,7 +29,9 @@
|
||||
"weekdayWednesday": "Mittwoch",
|
||||
"weekdayThursday": "Donnerstag",
|
||||
"weekdayFriday": "Freitag",
|
||||
"weekdaySaturday": "Samstag"
|
||||
"weekdaySaturday": "Samstag",
|
||||
"sessionSkills": "Session-Skills",
|
||||
"sessionSkillsHint": "Gilt für jede Aufgabe. Kombiniert sich mit Listen- und Aufgaben-Auswahl."
|
||||
},
|
||||
"worktrees": {
|
||||
"strategy": "Strategie",
|
||||
@@ -63,11 +69,50 @@
|
||||
"daySa": "Sa",
|
||||
"daySu": "So"
|
||||
},
|
||||
"onlineInbox": {
|
||||
"tabHeader": "Online-Posteingang",
|
||||
"enabledLabel": "Online-Posteingang-Sync aktivieren",
|
||||
"restartHint": "Aktivieren oder Deaktivieren wird erst nach einem Worker-Neustart wirksam.",
|
||||
"apiBaseUrlLabel": "API-Basis-URL",
|
||||
"apiBaseUrlPlaceholder": "https://inbox.claudedo.kuns.dev",
|
||||
"authorityLabel": "Zitadel-Authority (Issuer-URL)",
|
||||
"authorityPlaceholder": "https://auth.example.com",
|
||||
"clientIdLabel": "Client-ID",
|
||||
"scopesLabel": "Scopes",
|
||||
"redirectUriLabel": "Redirect-URI",
|
||||
"pollIntervalLabel": "Abfrageintervall (Sekunden)",
|
||||
"statusSection": "AUTH-STATUS",
|
||||
"signedInStatus": "Angemeldet",
|
||||
"signedOutStatus": "Nicht angemeldet",
|
||||
"signInButton": "Im Browser anmelden",
|
||||
"signOutButton": "Abmelden",
|
||||
"configSection": "KONFIGURATION",
|
||||
"saveButton": "Konfiguration speichern"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "geerbt · Liste",
|
||||
"inheritedFromGlobal": "geerbt · Global",
|
||||
"overrideBadge": "überschrieben",
|
||||
"resetToInherited": "Auf geerbt zurücksetzen"
|
||||
},
|
||||
"agentEditor": {
|
||||
"model": "Modell",
|
||||
"maxTurns": "Max. Durchläufe",
|
||||
"systemPrompt": "System-Prompt (angehängt)",
|
||||
"promptPrepended": "Wird automatisch vorangestellt:",
|
||||
"agentFile": "Agent-Datei",
|
||||
"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": {
|
||||
@@ -87,15 +132,22 @@
|
||||
"ctxMarkAs": "Markieren als",
|
||||
"ctxMarkDone": "Erledigt",
|
||||
"ctxMarkCancelled": "Abgebrochen",
|
||||
"ctxRunInteractively": "Interaktiv ausführen",
|
||||
"ctxOpenConPtySession": "ConPTY-Sitzung öffnen",
|
||||
"ctxPickUpInTerminal": "Im Terminal fortsetzen",
|
||||
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
||||
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
||||
"ctxFinalizePlanningSession": "Plan finalisieren",
|
||||
"ctxDiscardPlanningSession": "Planungssitzung verwerfen",
|
||||
"ctxQueueSubtasks": "Teilaufgaben nacheinander einreihen",
|
||||
"ctxScheduleFor": "Planen für...",
|
||||
"ctxClearSchedule": "Zeitplan entfernen",
|
||||
"ctxRemoveFromMyDay": "Aus Mein Tag entfernen",
|
||||
"ctxAddToMyDay": "Zu Mein Tag hinzufügen",
|
||||
"badgeDraft": "ENTWURF",
|
||||
"badgePlanned": "GEPLANT",
|
||||
"badgeManual": "MANUELL",
|
||||
"manualTip": "Manuelle Erinnerung — Claude nimmt sie nie auf",
|
||||
"ctxMarkManual": "Als manuell markieren",
|
||||
"ctxMarkClaudeTask": "Als Claude-Aufgabe markieren",
|
||||
"approve": "Genehmigen",
|
||||
"approveTip": "Genehmigen — als Erledigt markieren",
|
||||
"reject": "Ablehnen",
|
||||
@@ -107,6 +159,7 @@
|
||||
"removeFromQueueTip": "Aus Warteschlange entfernen",
|
||||
"toggleSubtasksTip": "Unteraufgaben ein-/ausklappen",
|
||||
"agentSuggestedTip": "Vom Agenten vorgeschlagen",
|
||||
"interactiveChipTip": "Interaktive Sitzung offen — klicken, um im Mission Control dorthin zu springen",
|
||||
"scheduleTitle": "Aufgabe planen",
|
||||
"scheduleWhen": "WANN",
|
||||
"scheduleConfirm": "Planen",
|
||||
@@ -115,7 +168,8 @@
|
||||
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
||||
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
||||
"rerun": "Erneut ausführen",
|
||||
"refineTip": "Aufgabe mit Claude verfeinern"
|
||||
"refineTip": "Aufgabe mit Claude verfeinern",
|
||||
"refiningTip": "Claude verfeinert diese Aufgabe…"
|
||||
},
|
||||
"lists": {
|
||||
"heading": "Listen",
|
||||
@@ -128,10 +182,12 @@
|
||||
"contextWorktrees": "Worktrees…",
|
||||
"contextOpenExplorer": "Im Explorer öffnen",
|
||||
"contextOpenTerminal": "Im Terminal öffnen",
|
||||
"contextLetClaude": "Claude machen lassen",
|
||||
"newList": "Neue Liste",
|
||||
"addReposTip": "Repos als Listen hinzufügen"
|
||||
},
|
||||
"details": {
|
||||
"pickUpInTerminalTip": "Diese Sitzung im Terminal fortsetzen",
|
||||
"deleteTaskTip": "Aufgabe löschen",
|
||||
"killSessionTip": "Laufende Sitzung beenden",
|
||||
"closeTip": "Schließen",
|
||||
@@ -139,16 +195,11 @@
|
||||
"starTip": "Favorit",
|
||||
"agentSettingsTip": "Agent-Einstellungen",
|
||||
"agentSettingsHeading": "Agent-Einstellungen (Überschreibungen)",
|
||||
"modelLabel": "Modell",
|
||||
"maxTurnsLabel": "Max. Durchläufe",
|
||||
"systemPromptLabel": "System-Prompt (angehängt)",
|
||||
"systemPromptPrepended": "Wird automatisch vorangestellt:",
|
||||
"agentFileLabel": "Agent-Datei",
|
||||
"mergeLabel": "MERGE",
|
||||
"mergeTargetLabel": "Merge-Ziel",
|
||||
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
|
||||
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
|
||||
"childOutcomesLabel": "VERBESSERUNGEN",
|
||||
"childOutcomesLabel": "TEILAUFGABEN",
|
||||
"stepsLabel": "SCHRITTE",
|
||||
"addStepPlaceholder": "Schritt hinzufügen...",
|
||||
"detailsLabel": "DETAILS",
|
||||
@@ -160,7 +211,23 @@
|
||||
"descriptionPlaceholder": "Aufgabendetails hinzufügen (Markdown unterstützt)...",
|
||||
"prepTitle": "Tagesvorbereitung",
|
||||
"planDay": "Tag planen",
|
||||
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen"
|
||||
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
||||
"attachments": {
|
||||
"sectionLabel": "ANHÄNGE",
|
||||
"dropToAttach": "Zum Anhängen ablegen",
|
||||
"addFile": "Datei hinzufügen…",
|
||||
"removeTip": "Anhang entfernen",
|
||||
"addedSummary": "✓ Hinzugefügt: {0} ({1} Datei(en))",
|
||||
"overLimitError": "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"
|
||||
},
|
||||
"sections": {
|
||||
"description": "Beschreibung",
|
||||
"steps": "Schritte",
|
||||
"files": "Dateien"
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"stopTip": "Agent stoppen",
|
||||
@@ -193,7 +260,44 @@
|
||||
"reviewContinueTip": "Dieses Feedback senden und die Aufgabe erneut ausführen",
|
||||
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
||||
},
|
||||
"missionControl": {
|
||||
"openInApp": "In App öffnen",
|
||||
"cancel": "Abbrechen",
|
||||
"detach": "Abdocken",
|
||||
"redock": "Andocken",
|
||||
"windowTitle": "Mission Control",
|
||||
"newSession": "Neue Sitzung",
|
||||
"clearFinished": "Erledigte entfernen",
|
||||
"empty": "Keine laufenden Aufgaben",
|
||||
"settings": "Einstellungen",
|
||||
"queue": "Warteschlange",
|
||||
"blocked": "Blockiert",
|
||||
"focusMode": "Fokus",
|
||||
"overviewMode": "Übersicht",
|
||||
"closeSession": "Sitzung schließen",
|
||||
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
||||
"conptyStarting": "Sitzung wird gestartet…",
|
||||
"mergeHelperTitle": "Merge-Helfer",
|
||||
"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": {
|
||||
"title": "Claude fragt nach",
|
||||
"placeholder": "Antwort eingeben…",
|
||||
"send": "Senden"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"logVisualizer": {
|
||||
"title": "WORKER-LOGS — LETZTE 30 MIN",
|
||||
"warnErrorOnly": "Nur Warnungen & Fehler",
|
||||
"refresh": "Aktualisieren",
|
||||
"empty": "Keine Logs in den letzten 30 Minuten.",
|
||||
"count": "{0} Einträge",
|
||||
"footerHint": "logs",
|
||||
"openTooltip": "Aktuelle Worker-Logs anzeigen"
|
||||
},
|
||||
"about": {
|
||||
"title": "ÜBER",
|
||||
"version": "Version",
|
||||
@@ -218,12 +322,10 @@
|
||||
"workingDirectoryPlaceholder": "(keines)",
|
||||
"browse": "Durchsuchen...",
|
||||
"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",
|
||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
||||
"model": "Modell",
|
||||
"maxTurns": "Max. Durchläufe",
|
||||
"systemPrompt": "System-Prompt (angehängt)",
|
||||
"agentFile": "Agent-Datei"
|
||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen"
|
||||
},
|
||||
"merge": {
|
||||
"title": "WORKTREE MERGEN",
|
||||
@@ -243,9 +345,6 @@
|
||||
"binary": "Binärdatei — kein Text-Diff",
|
||||
"empty": "Kein Inhalt"
|
||||
},
|
||||
"worktree": {
|
||||
"title": "Worktree"
|
||||
},
|
||||
"worktreesOverview": {
|
||||
"refresh": "Aktualisieren",
|
||||
"cleanupFinished": "Abgeschlossene aufräumen",
|
||||
@@ -280,6 +379,18 @@
|
||||
"forgetFolders": "Ordner vergessen",
|
||||
"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": {
|
||||
"title": "UNVOLLENDETE PLANUNGSSITZUNG",
|
||||
"windowTitle": "Unvollendete Planungssitzung",
|
||||
@@ -368,8 +479,6 @@
|
||||
"abort": "Diesen Merge abbrechen"
|
||||
},
|
||||
"diff": {
|
||||
"windowTitle": "Planung — Kombiniertes Diff",
|
||||
"modalTitle": "PLANUNG — KOMBINIERTES DIFF",
|
||||
"previewCombined": "Kombinierte Vorschau",
|
||||
"loading": "Wird geladen…"
|
||||
}
|
||||
@@ -378,13 +487,17 @@
|
||||
"windowTitle": "Merge-Konflikte lösen",
|
||||
"modalTitle": "KONFLIKTE LÖSEN",
|
||||
"loading": "Konflikte werden geladen…",
|
||||
"current": "Aktuell (unsere)",
|
||||
"incoming": "Eingehend (ihre)",
|
||||
"mergedResult": "Zusammengeführtes Ergebnis",
|
||||
"acceptCurrent": "Aktuelle übernehmen",
|
||||
"acceptIncoming": "Eingehende übernehmen",
|
||||
"acceptBoth": "Beide übernehmen",
|
||||
"editManually": "Manuell bearbeiten",
|
||||
"ours": "MAIN · Ziel-Branch",
|
||||
"result": "ERGEBNIS",
|
||||
"theirs": "INCOMING · Task-Branch",
|
||||
"binaryHint": "Binärdateien können hier nicht zusammengeführt werden — brich ab und löse sie in deinem Editor:",
|
||||
"prevConflict": "Vorheriger Konflikt (Umschalt+F8)",
|
||||
"nextConflict": "Nächster Konflikt (F8)",
|
||||
"conflictMap": "Konflikte in dieser Datei — Marker anklicken zum Springen",
|
||||
"acceptOurs": "Main hinzufügen",
|
||||
"acceptTheirs": "Incoming hinzufügen",
|
||||
"removeOurs": "Main entfernen",
|
||||
"removeTheirs": "Incoming entfernen",
|
||||
"continue": "Lösen & fortfahren",
|
||||
"abort": "Merge abbrechen"
|
||||
},
|
||||
@@ -401,6 +514,8 @@
|
||||
"shell": {
|
||||
"menu": {
|
||||
"help": "Hilfe",
|
||||
"worker": "Worker",
|
||||
"repositories": "Repositories",
|
||||
"checkForUpdates": "Nach Updates suchen",
|
||||
"restartWorker": "Worker neu starten",
|
||||
"worktrees": "Worktrees…",
|
||||
@@ -417,18 +532,20 @@
|
||||
"vm": {
|
||||
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
||||
"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" },
|
||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "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 Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" },
|
||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" },
|
||||
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen." },
|
||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "pickUpInTerminalFailed": "Im Terminal fortsetzen fehlgeschlagen: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}" },
|
||||
"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)." },
|
||||
"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}" },
|
||||
"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}" },
|
||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {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}" },
|
||||
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen 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." },
|
||||
"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." },
|
||||
"listSettings": { "untitled": "Unbenannt" },
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
"tabWorktrees": "Worktrees",
|
||||
"tabFiles": "Files",
|
||||
"tabPrime": "Prime Claude",
|
||||
"tabSkills": "Skills",
|
||||
"general": {
|
||||
"defaultInstructions": "Default instructions",
|
||||
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
||||
"model": "Model",
|
||||
"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",
|
||||
"maxParallelExecutions": "Max parallel executions",
|
||||
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
||||
@@ -25,7 +29,9 @@
|
||||
"weekdayWednesday": "Wednesday",
|
||||
"weekdayThursday": "Thursday",
|
||||
"weekdayFriday": "Friday",
|
||||
"weekdaySaturday": "Saturday"
|
||||
"weekdaySaturday": "Saturday",
|
||||
"sessionSkills": "Session skills",
|
||||
"sessionSkillsHint": "Applied to every task. Combines with list- and task-level selections."
|
||||
},
|
||||
"worktrees": {
|
||||
"strategy": "Strategy",
|
||||
@@ -63,11 +69,50 @@
|
||||
"daySa": "Sa",
|
||||
"daySu": "Su"
|
||||
},
|
||||
"onlineInbox": {
|
||||
"tabHeader": "Online Inbox",
|
||||
"enabledLabel": "Enable online inbox sync",
|
||||
"restartHint": "Enabling or disabling takes effect after a Worker restart.",
|
||||
"apiBaseUrlLabel": "API base URL",
|
||||
"apiBaseUrlPlaceholder": "https://inbox.claudedo.kuns.dev",
|
||||
"authorityLabel": "Zitadel authority (issuer URL)",
|
||||
"authorityPlaceholder": "https://auth.example.com",
|
||||
"clientIdLabel": "Client ID",
|
||||
"scopesLabel": "Scopes",
|
||||
"redirectUriLabel": "Redirect URI",
|
||||
"pollIntervalLabel": "Poll interval (seconds)",
|
||||
"statusSection": "AUTH STATUS",
|
||||
"signedInStatus": "Signed in",
|
||||
"signedOutStatus": "Not signed in",
|
||||
"signInButton": "Sign in via browser",
|
||||
"signOutButton": "Sign out",
|
||||
"configSection": "CONFIGURATION",
|
||||
"saveButton": "Save config"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "inherited · List",
|
||||
"inheritedFromGlobal": "inherited · Global",
|
||||
"overrideBadge": "override",
|
||||
"resetToInherited": "Reset to inherited"
|
||||
},
|
||||
"agentEditor": {
|
||||
"model": "Model",
|
||||
"maxTurns": "Max turns",
|
||||
"systemPrompt": "System prompt (appended)",
|
||||
"promptPrepended": "Prepended automatically:",
|
||||
"agentFile": "Agent file",
|
||||
"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": {
|
||||
@@ -87,15 +132,22 @@
|
||||
"ctxMarkAs": "Mark as",
|
||||
"ctxMarkDone": "Done",
|
||||
"ctxMarkCancelled": "Cancelled",
|
||||
"ctxRunInteractively": "Run interactively",
|
||||
"ctxOpenConPtySession": "Open ConPTY session",
|
||||
"ctxPickUpInTerminal": "Pick up in terminal",
|
||||
"ctxOpenPlanningSession": "Open planning Session",
|
||||
"ctxResumePlanningSession": "Resume planning Session",
|
||||
"ctxFinalizePlanningSession": "Finalize plan",
|
||||
"ctxDiscardPlanningSession": "Discard planning session",
|
||||
"ctxQueueSubtasks": "Queue subtasks sequentially",
|
||||
"ctxScheduleFor": "Schedule for...",
|
||||
"ctxClearSchedule": "Clear schedule",
|
||||
"ctxRemoveFromMyDay": "Remove from My Day",
|
||||
"ctxAddToMyDay": "Add to My Day",
|
||||
"badgeDraft": "DRAFT",
|
||||
"badgePlanned": "PLANNED",
|
||||
"badgeManual": "MANUAL",
|
||||
"manualTip": "Manual reminder — Claude never picks this up",
|
||||
"ctxMarkManual": "Mark as manual",
|
||||
"ctxMarkClaudeTask": "Mark as Claude task",
|
||||
"approve": "Approve",
|
||||
"approveTip": "Approve — mark Done",
|
||||
"reject": "Reject",
|
||||
@@ -107,6 +159,7 @@
|
||||
"removeFromQueueTip": "Remove from queue",
|
||||
"toggleSubtasksTip": "Expand / collapse subtasks",
|
||||
"agentSuggestedTip": "Suggested by the agent",
|
||||
"interactiveChipTip": "Interactive session open — click to jump to it in Mission Control",
|
||||
"scheduleTitle": "Schedule task",
|
||||
"scheduleWhen": "WHEN",
|
||||
"scheduleConfirm": "Schedule",
|
||||
@@ -115,7 +168,8 @@
|
||||
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
||||
"feedbackPlaceholder": "What should the agent fix?",
|
||||
"rerun": "Re-run",
|
||||
"refineTip": "Refine this task with Claude"
|
||||
"refineTip": "Refine this task with Claude",
|
||||
"refiningTip": "Claude is refining this task…"
|
||||
},
|
||||
"lists": {
|
||||
"heading": "Lists",
|
||||
@@ -128,10 +182,12 @@
|
||||
"contextWorktrees": "Worktrees…",
|
||||
"contextOpenExplorer": "Open in Explorer",
|
||||
"contextOpenTerminal": "Open in Terminal",
|
||||
"contextLetClaude": "Let Claude handle it",
|
||||
"newList": "New list",
|
||||
"addReposTip": "Add repos as lists"
|
||||
},
|
||||
"details": {
|
||||
"pickUpInTerminalTip": "Pick up this session in a terminal",
|
||||
"deleteTaskTip": "Delete task",
|
||||
"killSessionTip": "Kill the running session",
|
||||
"closeTip": "Close",
|
||||
@@ -139,16 +195,11 @@
|
||||
"starTip": "Star",
|
||||
"agentSettingsTip": "Agent settings",
|
||||
"agentSettingsHeading": "Agent settings (overrides)",
|
||||
"modelLabel": "Model",
|
||||
"maxTurnsLabel": "Max turns",
|
||||
"systemPromptLabel": "System prompt (appended)",
|
||||
"systemPromptPrepended": "Prepended automatically:",
|
||||
"agentFileLabel": "Agent file",
|
||||
"mergeLabel": "MERGE",
|
||||
"mergeTargetLabel": "Merge target",
|
||||
"reviewCombinedDiff": "Review combined diff",
|
||||
"mergeAllSubtasks": "Merge all subtasks",
|
||||
"childOutcomesLabel": "IMPROVEMENTS",
|
||||
"childOutcomesLabel": "SUBTASKS",
|
||||
"stepsLabel": "STEPS",
|
||||
"addStepPlaceholder": "Add a step...",
|
||||
"detailsLabel": "DETAILS",
|
||||
@@ -160,7 +211,23 @@
|
||||
"descriptionPlaceholder": "Add task details (markdown supported)...",
|
||||
"prepTitle": "Daily prep",
|
||||
"planDay": "Plan day",
|
||||
"prepEmpty": "No prep run today yet — click Plan day"
|
||||
"prepEmpty": "No prep run today yet — click Plan day",
|
||||
"attachments": {
|
||||
"sectionLabel": "ATTACHMENTS",
|
||||
"dropToAttach": "Drop to attach",
|
||||
"addFile": "Add file…",
|
||||
"removeTip": "Remove attachment",
|
||||
"addedSummary": "✓ Added {0} ({1} file(s))",
|
||||
"overLimitError": "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"
|
||||
},
|
||||
"sections": {
|
||||
"description": "Description",
|
||||
"steps": "Steps",
|
||||
"files": "Files"
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"stopTip": "Stop agent",
|
||||
@@ -193,7 +260,44 @@
|
||||
"reviewContinueTip": "Send this feedback and re-run the task",
|
||||
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
||||
},
|
||||
"missionControl": {
|
||||
"openInApp": "Open in app",
|
||||
"cancel": "Cancel",
|
||||
"detach": "Detach",
|
||||
"redock": "Re-dock",
|
||||
"windowTitle": "Mission Control",
|
||||
"newSession": "New session",
|
||||
"clearFinished": "Clear finished",
|
||||
"empty": "No running tasks",
|
||||
"settings": "Settings",
|
||||
"queue": "Queue",
|
||||
"blocked": "Blocked",
|
||||
"focusMode": "Focus",
|
||||
"overviewMode": "Overview",
|
||||
"closeSession": "Close session",
|
||||
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
||||
"conptyStarting": "Starting session…",
|
||||
"mergeHelperTitle": "Merge Helper",
|
||||
"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": {
|
||||
"title": "Claude is asking",
|
||||
"placeholder": "Type your answer…",
|
||||
"send": "Send"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"logVisualizer": {
|
||||
"title": "WORKER LOGS — LAST 30 MIN",
|
||||
"warnErrorOnly": "Warnings & errors only",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No logs in the last 30 minutes.",
|
||||
"count": "{0} entries",
|
||||
"footerHint": "logs",
|
||||
"openTooltip": "View recent worker logs"
|
||||
},
|
||||
"about": {
|
||||
"title": "ABOUT",
|
||||
"version": "Version",
|
||||
@@ -218,12 +322,10 @@
|
||||
"workingDirectoryPlaceholder": "(none)",
|
||||
"browse": "Browse...",
|
||||
"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",
|
||||
"resetAgentSettings": "Reset agent settings",
|
||||
"model": "Model",
|
||||
"maxTurns": "Max turns",
|
||||
"systemPrompt": "System prompt (appended)",
|
||||
"agentFile": "Agent file"
|
||||
"resetAgentSettings": "Reset agent settings"
|
||||
},
|
||||
"merge": {
|
||||
"title": "MERGE WORKTREE",
|
||||
@@ -243,9 +345,6 @@
|
||||
"binary": "Binary file — no text diff",
|
||||
"empty": "No content"
|
||||
},
|
||||
"worktree": {
|
||||
"title": "Worktree"
|
||||
},
|
||||
"worktreesOverview": {
|
||||
"refresh": "Refresh",
|
||||
"cleanupFinished": "Cleanup finished",
|
||||
@@ -280,6 +379,18 @@
|
||||
"forgetFolders": "Forget folders",
|
||||
"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": {
|
||||
"title": "UNFINISHED PLANNING SESSION",
|
||||
"windowTitle": "Unfinished planning session",
|
||||
@@ -368,8 +479,6 @@
|
||||
"abort": "Abort this merge"
|
||||
},
|
||||
"diff": {
|
||||
"windowTitle": "Planning — Combined diff",
|
||||
"modalTitle": "PLANNING — COMBINED DIFF",
|
||||
"previewCombined": "Preview combined",
|
||||
"loading": "Loading…"
|
||||
}
|
||||
@@ -378,13 +487,17 @@
|
||||
"windowTitle": "Resolve merge conflicts",
|
||||
"modalTitle": "RESOLVE CONFLICTS",
|
||||
"loading": "Loading conflicts…",
|
||||
"current": "Current (ours)",
|
||||
"incoming": "Incoming (theirs)",
|
||||
"mergedResult": "Merged result",
|
||||
"acceptCurrent": "Accept Current",
|
||||
"acceptIncoming": "Accept Incoming",
|
||||
"acceptBoth": "Accept Both",
|
||||
"editManually": "Edit manually",
|
||||
"ours": "MAIN · merge target",
|
||||
"result": "RESULT",
|
||||
"theirs": "INCOMING · task branch",
|
||||
"binaryHint": "Binary files can't be merged here — abort and resolve them in your editor:",
|
||||
"prevConflict": "Previous conflict (Shift+F8)",
|
||||
"nextConflict": "Next conflict (F8)",
|
||||
"conflictMap": "Conflicts in this file — click a marker to jump",
|
||||
"acceptOurs": "Add main",
|
||||
"acceptTheirs": "Add incoming",
|
||||
"removeOurs": "Remove main",
|
||||
"removeTheirs": "Remove incoming",
|
||||
"continue": "Resolve & continue",
|
||||
"abort": "Abort merge"
|
||||
},
|
||||
@@ -401,6 +514,8 @@
|
||||
"shell": {
|
||||
"menu": {
|
||||
"help": "Help",
|
||||
"worker": "Worker",
|
||||
"repositories": "Repositories",
|
||||
"checkForUpdates": "Check for updates",
|
||||
"restartWorker": "Restart worker",
|
||||
"worktrees": "Worktrees…",
|
||||
@@ -417,18 +532,20 @@
|
||||
"vm": {
|
||||
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
||||
"shell": { "restartingWorker": "Restarting worker…" },
|
||||
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Improvements", "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" },
|
||||
"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 Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" },
|
||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" },
|
||||
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show." },
|
||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "pickUpInTerminalFailed": "Pick up in terminal failed: {0}", "approveFailed": "Approve & merge failed: {0}" },
|
||||
"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)." },
|
||||
"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}" },
|
||||
"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}" },
|
||||
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {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}" },
|
||||
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove 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)." },
|
||||
"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." },
|
||||
"listSettings": { "untitled": "Untitled" },
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace ClaudeDo.Releases;
|
||||
|
||||
public sealed record InstallerAssetMatch(ReleaseAsset Asset, string Version);
|
||||
|
||||
public enum SelfUpdateDecisionKind
|
||||
{
|
||||
NoUpdate,
|
||||
UpdateAvailable,
|
||||
}
|
||||
|
||||
public sealed record SelfUpdateDecision(
|
||||
SelfUpdateDecisionKind Kind,
|
||||
string? LatestVersion = null,
|
||||
ReleaseAsset? InstallerAsset = null,
|
||||
ReleaseAsset? ChecksumsAsset = null);
|
||||
@@ -1,126 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClaudeDo.Releases;
|
||||
|
||||
public static partial class SelfUpdater
|
||||
{
|
||||
[GeneratedRegex(@"^ClaudeDo\.Installer-(?<version>[\d\.]+)\.exe$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex InstallerAssetRegex();
|
||||
|
||||
public static InstallerAssetMatch? FindInstallerAsset(IEnumerable<ReleaseAsset> assets)
|
||||
{
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
var m = InstallerAssetRegex().Match(asset.Name);
|
||||
if (m.Success)
|
||||
{
|
||||
return new InstallerAssetMatch(asset, m.Groups["version"].Value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<SelfUpdateDecision> DecideUpdateAsync(
|
||||
IReleaseClient releases,
|
||||
string currentVersion,
|
||||
CancellationToken ct)
|
||||
{
|
||||
GiteaRelease? release;
|
||||
try
|
||||
{
|
||||
release = await releases.GetLatestReleaseAsync(ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||
{
|
||||
return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate);
|
||||
}
|
||||
|
||||
if (release is null)
|
||||
return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate);
|
||||
|
||||
var match = FindInstallerAsset(release.Assets);
|
||||
if (match is null)
|
||||
return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate);
|
||||
|
||||
var cmp = VersionComparer.Compare(match.Version, currentVersion);
|
||||
if (!cmp.IsNewer)
|
||||
return new SelfUpdateDecision(SelfUpdateDecisionKind.NoUpdate);
|
||||
|
||||
var checksums = release.Assets.FirstOrDefault(
|
||||
a => string.Equals(a.Name, "checksums.txt", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return new SelfUpdateDecision(
|
||||
SelfUpdateDecisionKind.UpdateAvailable,
|
||||
LatestVersion: match.Version,
|
||||
InstallerAsset: match.Asset,
|
||||
ChecksumsAsset: checksums);
|
||||
}
|
||||
|
||||
public static async Task<bool> HandleReplaceSelfAsync(
|
||||
string oldPath,
|
||||
string currentExePath,
|
||||
Func<string, bool> launchProcess,
|
||||
int maxWaitMs = 5000)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddMilliseconds(maxWaitMs);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(oldPath))
|
||||
{
|
||||
File.Delete(oldPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(oldPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
File.Copy(currentExePath, oldPath, overwrite: false);
|
||||
return launchProcess(oldPath);
|
||||
}
|
||||
|
||||
public static async Task<string?> DownloadAndVerifyAsync(
|
||||
IReleaseClient releases,
|
||||
ReleaseAsset installerAsset,
|
||||
ReleaseAsset checksumsAsset,
|
||||
string tempDir,
|
||||
IProgress<long> progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var installerPath = Path.Combine(tempDir, installerAsset.Name);
|
||||
var checksumsPath = Path.Combine(tempDir, "checksums.txt");
|
||||
|
||||
try
|
||||
{
|
||||
await releases.DownloadAsync(installerAsset.BrowserDownloadUrl, installerPath, progress, ct);
|
||||
await releases.DownloadAsync(checksumsAsset.BrowserDownloadUrl, checksumsPath, new Progress<long>(_ => { }), ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or IOException or TaskCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var checksumsText = await File.ReadAllTextAsync(checksumsPath, ct);
|
||||
var map = ChecksumVerifier.ParseChecksumsFile(checksumsText);
|
||||
if (!map.TryGetValue(installerAsset.Name, out var expected))
|
||||
return null;
|
||||
|
||||
return ChecksumVerifier.Verify(installerPath, expected) ? installerPath : null;
|
||||
}
|
||||
}
|
||||
+42
-33
@@ -8,56 +8,65 @@ MVVM with CommunityToolkit.Mvvm source generators:
|
||||
- `[ObservableProperty]` for bindable properties
|
||||
- `[RelayCommand]` for commands (supports async and CanExecute)
|
||||
- All ViewModels inherit `ViewModelBase` (extends `ObservableObject`)
|
||||
- All views use compiled bindings (`x:DataType`)
|
||||
|
||||
## Views
|
||||
## Layout: Islands
|
||||
|
||||
- **MainWindow** — 3-column DockPanel layout (lists | tasks | detail) with GridSplitter, status bar at bottom
|
||||
- **TaskListView** — ListBox of tasks with add/edit/delete toolbar
|
||||
- **TaskDetailView** — Task info, live log output, worktree section (merge/keep/discard)
|
||||
- **TaskEditorView** — Modal dialog for task create/edit
|
||||
- **ListEditorView** — Modal dialog for list create/edit
|
||||
- **StatusBarView** — Connection status indicator, active task display
|
||||
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath/MaxTurns, each showing the inherited (global) value with a source-aware "inherited · Global / override" badge and a reset-to-inherited button; also deletes the list (and its tasks) via a confirmed "Delete list" button. Opened via context menu or gear button on a list row.
|
||||
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
|
||||
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/MaxTurns/AgentPath (override semantics, each showing the resolved inherited value as a placeholder plus a source-aware "inherited · List / inherited · Global / override" badge and reset button via the reusable `InheritedBadge` control + `InheritanceResolver`) and a SystemPrompt text box (additive — shows the inherited prompt as a "prepended automatically" note). Disabled while task is running. When notes mode is active (`IsNotesMode`), it hosts **NotesEditorView** instead of the task detail. When prep mode is active (`IsPrepMode`), it hosts the daily-prep panel (Plan day button, empty-state hint, embedded **SessionTerminalView**). The task header, metadata footer (delete/close), and **AgentStripView** are gated on `IsTaskDetailVisible` — they are hidden in both notes and prep mode.
|
||||
- **WeeklyReportModalView** — opened from Help menu ("Wochenbericht…"); date-range pickers default to "since last standup weekday → today"; Generate/Regenerate button; renders markdown via MarkdownView; reports are cached per range.
|
||||
- **NotesEditorView** — day navigator (prev/next/date-picker/Today), bullet add/edit/delete for daily notes.
|
||||
- **SessionTerminalView** — reusable log terminal; exposes StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`. Used for both the task `Log` and the prep `PrepLog`.
|
||||
- **SettingsModalView** — Prime Claude tab contains a `DailyPrepMaxTasks` numeric editor.
|
||||
- **TasksIslandView** (MyDay header) — icon buttons visible only when `IsMyDayList`: broom icon = `ClearDayCommand`, stroked-sun icon ("Plan My Day") = `ShowPrepLogCommand`.
|
||||
`MainWindow` hosts three "islands" (lists | tasks | details). There is no MainWindowViewModel, StatusBarView, or task/list editor modal — the root coordinator is **IslandsShellViewModel**, and task/list editing happens inline in the islands.
|
||||
|
||||
All views use compiled bindings (`x:DataType`).
|
||||
```
|
||||
ViewModels/
|
||||
IslandsShellViewModel.cs — root coordinator
|
||||
Islands/ — ListsIsland, TasksIsland, DetailsIsland, TaskRow, ListNavItem,
|
||||
NotesEditor, MergePreviewPresenter
|
||||
Agent/ — AgentConfigEditorViewModel (scope-parameterized: List | Task)
|
||||
Modals/ — About, DiffViewer (+ DiffModels), ListSettings, Merge, MergeHelperSelection,
|
||||
RepoImport, Settings (+ Settings/ tab VMs), UnfinishedPlanning, WeeklyReport,
|
||||
WorkerConnection, WorktreesOverview, UnifiedDiffParser
|
||||
Conflicts/ — ConflictResolverViewModel + ConflictModels (MergeFile/MergeFileSegment/MergeConflictBlock)
|
||||
Views/ — mirrors the VM layout; Islands/Detail/ holds TaskHeaderBar,
|
||||
DescriptionStepsCard, WorkConsole; plus AgentStripView, SessionTerminalView
|
||||
Views/Controls/ — MarkdownView, ModalShell, ThemedDatePicker, DiffLinesView, InheritedBadge, AgentConfigEditor
|
||||
Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyles.axaml
|
||||
(component styles + the filled icon geometry library)
|
||||
```
|
||||
|
||||
## ViewModels
|
||||
|
||||
- **MainWindowViewModel** — root coordinator; manages list collection, selected list, dialog creation via `Func<T>` factories
|
||||
- **TaskListViewModel** — manages task collection for selected list; handles CRUD, "Run Now"
|
||||
- **TaskDetailViewModel** — displays task details, streams live log, controls worktree operations
|
||||
- **TaskItemViewModel** / **ListItemViewModel** — lightweight display VMs
|
||||
- **TaskEditorViewModel** / **ListEditorViewModel** — dialog VMs with validation
|
||||
- **StatusBarViewModel** — connection state and active tasks
|
||||
- **WeeklyReportModalViewModel** — drives the weekly report modal
|
||||
- **NotesEditorViewModel** — manages daily note bullet CRUD for the selected day
|
||||
- **DetailsIslandViewModel** gains `IsNotesMode`, `ShowNotes()`, and hosts `NotesEditorViewModel`. Also gains daily-prep mode: `IsPrepMode`, `PrepLog` (`ObservableCollection<LogLineViewModel>`), `ShowPrep()`, `PlanDayCommand` (calls `RunDailyPrepNowAsync`), `ShowPrepEmptyState`, and computed `IsTaskDetailVisible` (= `!IsNotesMode && !IsPrepMode`). Subscribes to `PrepStartedEvent`, `PrepLineEvent`, `PrepFinishedEvent`; streams lines into `PrepLog` via `StreamLineFormatter` (same path as task logs). On open, if log is empty and no run is in progress, loads persisted last run via `GetLastPrepLogAsync`. The WorkConsole Session tab gains a mergeability indicator (`MergePreviewPresenter`) and a single-task Merge button; indicator is populated via `PreviewMergeAsync` and displayed for tasks in WaitingForReview.
|
||||
- **TasksIslandViewModel** gains a pinned "Notes" pseudo-row (`ShowNotesRow`, `OpenNotesCommand`, `NotesRequested` event) that switches the Details island to notes mode. Also gains `IsMyDayList` (true when selected list is `smart:my-day`), `ShowPrepLogCommand` (raises `PrepRequested` event → shell calls `Details.ShowPrep()`), and `ClearDayCommand` (calls `ClearMyDayAsync`).
|
||||
- **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`.
|
||||
- **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), `PickUpInTerminalAsync`, `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`, 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`; 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`.
|
||||
- **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, 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), `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` opens an ad-hoc ConPTY tile running the five-phase handler prompt), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`, `LogVisualizerViewModel` (worker logs, last 30 min, all levels + a warn/error-only filter; loads via `GetRecentLogsAsync`).
|
||||
- **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`).
|
||||
|
||||
## Services
|
||||
|
||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync, ContinueTaskAsync, ResetTaskAsync, GetAgentsAsync, UpdateAppSettingsAsync, UpdateListAsync, UpdateListConfigAsync, GetListConfigAsync, UpdateTaskAgentSettingsAsync, `ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, `PreviewMergeAsync(taskId, targetBranch) -> MergePreviewDto`, `MergeTaskAsync`, `GetWeekReportAsync`, `GenerateWeekReportAsync`, `GetDailyNotesAsync`, `AddDailyNoteAsync`, `UpdateDailyNoteAsync`, `DeleteDailyNoteAsync`, `RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated, ListUpdated, `PrepStartedEvent`, `PrepLineEvent`, `PrepFinishedEvent`
|
||||
- **INotesApi** / **WorkerNotesApi** — thin wrapper over the daily-note WorkerClient methods (mirrors `IPrimeScheduleApi`). UI DTO: `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||
- **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), pick-up-in-terminal + 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`). 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`.
|
||||
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
||||
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
||||
- **InheritanceResolver** — resolves the task → list → global override chain to `(value, source)` for the inherited badges.
|
||||
- **RepoScanner**, **InstallArtifactLocator**/**InstallerLocator**/**WorkerLocator**, **ForegroundHelper** (Win32 foreground before launching a terminal), **FocusClearing**.
|
||||
|
||||
## Converters
|
||||
|
||||
- **StatusColorConverter** — task status string -> color (Queued=Blue, Running=Orange, Done=Green, Failed=Red, Manual=Gray)
|
||||
- **ConnectionColorConverter** — connection state -> color (Online=Green, Offline=Red)
|
||||
`StatusColorConverter` (+ `ConnectionColorConverter` in the same file), `WorkerLogLevelToBrushConverter`, `DotBrushConverter`, `EqStatusConverter`, `IconKeyConverter`, `CheckboxBorderConverter`, `StrikeIfTrueConverter`, `BoolToItalicConverter`, `BoolToDraftOpacityConverter`, `NotNullToBoolConverter`, `UpperCaseConverter`, `DateOnlyToDateTimeConverter`.
|
||||
|
||||
## Dialog Pattern
|
||||
|
||||
Editor dialogs use `TaskCompletionSource<bool>` — the dialog sets the result on save/cancel, and the caller awaits the TCS.
|
||||
Modals use `TaskCompletionSource` results behind the reusable `ModalShell` control — the dialog sets the result on save/cancel, and the caller awaits the TCS.
|
||||
|
||||
## Notes
|
||||
|
||||
- Context menus are on both list items and task items
|
||||
- Right-click selects the item before showing the context 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
|
||||
- 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.
|
||||
- `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`.
|
||||
- `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).
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.0.0" />
|
||||
<PackageReference Include="Avalonia" Version="12.0.4" />
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="12.0.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="2.0.3" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
||||
<PackageReference Include="Iciclecreek.Avalonia.Terminal" Version="2.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public sealed class LogKindForegroundConverter : IValueConverter
|
||||
{
|
||||
private static IBrush? Resolve(string key)
|
||||
{
|
||||
if (Application.Current is { } app &&
|
||||
app.Resources.TryGetResource(key, app.ActualThemeVariant, out var res) &&
|
||||
res is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value is LogKind kind ? kind switch
|
||||
{
|
||||
LogKind.Sys => "TextMuteBrush",
|
||||
LogKind.Tool => "SageBrush",
|
||||
LogKind.Claude => "TextBrush",
|
||||
LogKind.Stdout => "TextDimBrush",
|
||||
LogKind.Stderr => "BloodBrush",
|
||||
LogKind.Done => "MossBrightBrush",
|
||||
LogKind.Msg => "TextDimBrush",
|
||||
LogKind.User => "AccentBrush",
|
||||
_ => "TextDimBrush",
|
||||
} : "TextDimBrush";
|
||||
|
||||
return Resolve(key) ?? AvaloniaProperty.UnsetValue;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public sealed class WorktreeStateColorConverter : IValueConverter
|
||||
{
|
||||
private static readonly ISolidColorBrush Active = new SolidColorBrush(Color.Parse("#42A5F5"));
|
||||
private static readonly ISolidColorBrush Merged = new SolidColorBrush(Color.Parse("#66BB6A"));
|
||||
private static readonly ISolidColorBrush Discarded = new SolidColorBrush(Color.Parse("#9E9E9E"));
|
||||
private static readonly ISolidColorBrush Kept = new SolidColorBrush(Color.Parse("#FFA726"));
|
||||
private static readonly ISolidColorBrush Default = new SolidColorBrush(Colors.Gray);
|
||||
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
value is WorktreeState state
|
||||
? state switch
|
||||
{
|
||||
WorktreeState.Active => Active,
|
||||
WorktreeState.Merged => Merged,
|
||||
WorktreeState.Discarded => Discarded,
|
||||
WorktreeState.Kept => Kept,
|
||||
_ => Default,
|
||||
}
|
||||
: Default;
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
<!-- Window control icons — filled geometries (PathIcon fills, not strokes) -->
|
||||
<StreamGeometry x:Key="Icon.WinMin">M4 9 H16 V11 H4 Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.WinMax">M4 4 H16 V6 H4 Z M4 14 H16 V16 H4 Z M4 4 H6 V16 H4 Z M14 4 H16 V16 H14 Z</StreamGeometry>
|
||||
<!-- Icon.Grid (four filled panes — Mission Control launcher) -->
|
||||
<StreamGeometry x:Key="Icon.Grid">M3 3 H9 V9 H3 Z M11 3 H17 V9 H11 Z M3 11 H9 V17 H3 Z M11 11 H17 V17 H11 Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.WinRestore">M4 7 H13 V9 H4 Z M4 14 H13 V16 H4 Z M4 7 H6 V16 H4 Z M11 7 H13 V16 H11 Z M7 4 H16 V6 H7 Z M14 4 H16 V13 H14 Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.WinClose">M4 5 L5 4 L16 15 L15 16 Z M15 4 L16 5 L5 16 L4 15 Z</StreamGeometry>
|
||||
<!-- Brand check glyph — filled rounded square with inset tick -->
|
||||
@@ -55,8 +57,8 @@
|
||||
<!-- 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>
|
||||
|
||||
<!-- Icon.Plus -->
|
||||
<StreamGeometry x:Key="Icon.Plus">M12 5v14M5 12h14</StreamGeometry>
|
||||
<!-- Icon.Plus — filled cross (PathIcon fills, so a stroke-only plus renders invisible) -->
|
||||
<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 -->
|
||||
<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>
|
||||
@@ -79,8 +81,11 @@
|
||||
<!-- Icon.Refine (stroke-rendered via Path.plan-icon — list lines + two sparkles) -->
|
||||
<StreamGeometry x:Key="Icon.Refine">M3,6 L13,6 M3,11 L11,11 M3,16 L9,16 M18.5,3 L19.28,5.22 L21.5,6 L19.28,6.78 L18.5,9 L17.72,6.78 L15.5,6 L17.72,5.22 Z M19.5,14.9 L19.85,16.15 L21.1,16.5 L19.85,16.85 L19.5,18.1 L19.15,16.85 L17.9,16.5 L19.15,16.15 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.X -->
|
||||
<StreamGeometry x:Key="Icon.X">M6 6l12 12M18 6L6 18</StreamGeometry>
|
||||
<!-- Icon.X — filled X outline (PathIcon fills, so a stroke-only X renders invisible) -->
|
||||
<StreamGeometry x:Key="Icon.X">M6.4 4.6 L12 10.2 L17.6 4.6 L19.4 6.4 L13.8 12 L19.4 17.6 L17.6 19.4 L12 13.8 L6.4 19.4 L4.6 17.6 L10.2 12 L4.6 6.4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Stop — filled square (stop / interrupt) -->
|
||||
<StreamGeometry x:Key="Icon.Stop">M4 4 H20 V20 H4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Check -->
|
||||
<StreamGeometry x:Key="Icon.Check">M4 12l5 5 11-11</StreamGeometry>
|
||||
@@ -88,6 +93,10 @@
|
||||
<!-- Icon.ArrowOut — filled arrow for "open external" button -->
|
||||
<StreamGeometry x:Key="Icon.ArrowOut">M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.ChevronRight / Icon.ChevronDown — filled expand/collapse chevrons (PathIcon fills) -->
|
||||
<StreamGeometry x:Key="Icon.ChevronRight">M9 5 L16 12 L9 19 L6.8 16.8 L11.6 12 L6.8 7.2 Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="Icon.ChevronDown">M5 9 L12 16 L19 9 L16.8 6.8 L12 11.6 L7.2 6.8 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Text — three filled horizontal bars (paragraph / description icon) -->
|
||||
<StreamGeometry x:Key="Icon.Text">M4 6 H20 V8 H4 Z M4 11 H20 V13 H4 Z M4 16 H14 V18 H4 Z</StreamGeometry>
|
||||
|
||||
@@ -107,6 +116,7 @@
|
||||
<SolidColorBrush x:Key="DraftBadgeBrush" Color="{StaticResource TextMuteColor}"/>
|
||||
<SolidColorBrush x:Key="PlanningBadgeBrush" Color="{StaticResource PeatColor}"/>
|
||||
<SolidColorBrush x:Key="PlannedBadgeBrush" Color="{StaticResource SageColor}"/>
|
||||
<SolidColorBrush x:Key="ManualBadgeBrush" Color="{StaticResource TextFaintColor}"/>
|
||||
|
||||
</Styles.Resources>
|
||||
|
||||
@@ -229,6 +239,67 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- parked → slate-blue: an Idle task still holding its Active worktree -->
|
||||
<Style Selector="Border.chip.parked">
|
||||
<Setter Property="Background" Value="#22303A" />
|
||||
<Setter Property="BorderBrush" Value="#3A5060" />
|
||||
</Style>
|
||||
<Style Selector="Border.chip.parked > TextBlock">
|
||||
<Setter Property="Foreground" Value="#8FB9D6" />
|
||||
</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) -->
|
||||
<!-- active → slate-blue (same hue as parked: a live worktree) -->
|
||||
<Style Selector="Border.chip.wt-active">
|
||||
<Setter Property="Background" Value="#22303A" />
|
||||
<Setter Property="BorderBrush" Value="#3A5060" />
|
||||
</Style>
|
||||
<Style Selector="Border.chip.wt-active > TextBlock">
|
||||
<Setter Property="Foreground" Value="#8FB9D6" />
|
||||
</Style>
|
||||
|
||||
<!-- merged → green -->
|
||||
<Style Selector="Border.chip.wt-merged">
|
||||
<Setter Property="Background" Value="{StaticResource DoneTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource DoneTintBorderBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.chip.wt-merged > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource StatusDoneBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- kept → amber -->
|
||||
<Style Selector="Border.chip.wt-kept">
|
||||
<Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.chip.wt-kept > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- discarded → muted gray (same as idle) -->
|
||||
<Style Selector="Border.chip.wt-discarded">
|
||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource LineBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.chip.wt-discarded > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- BUTTONS -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -355,6 +426,8 @@
|
||||
<BrushTransition Property="Background" Duration="0:0:0.12" />
|
||||
<BrushTransition Property="BorderBrush" Duration="0:0:0.12" />
|
||||
<ThicknessTransition Property="Margin" Duration="0:0:0.15" />
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.12" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
@@ -362,9 +435,16 @@
|
||||
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.task-row.selected">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<!-- "Grabbed" row: lift + slight scale + lower opacity + shadow while the custom drag runs. -->
|
||||
<Style Selector="Border.task-row.dragging">
|
||||
<Setter Property="Opacity" Value="0.55" />
|
||||
<Setter Property="RenderTransform" Value="scale(1.03)" />
|
||||
<Setter Property="BoxShadow" Value="0 10 26 0 #66000000" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Checkbox indicator (the 18px circle that replaces the native CheckBox template) -->
|
||||
<Style Selector="Ellipse.task-check">
|
||||
@@ -390,6 +470,25 @@
|
||||
</Style.Animations>
|
||||
</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 -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -467,6 +566,10 @@
|
||||
<Style Selector="Border.terminal TextBlock[Tag=log-msg]">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
||||
</Style>
|
||||
<!-- log-user: user's own messages in interactive sessions — accent color to stand out -->
|
||||
<Style Selector="Border.terminal TextBlock[Tag=log-user]">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TERMINAL HEADER -->
|
||||
@@ -871,14 +974,9 @@
|
||||
<Setter Property="Padding" Value="8,5" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="0:0:0.10"/>
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Border.subtask-row:pointerover">
|
||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||
<Setter Property="Background" Value="{StaticResource Surface3Brush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.subtask-row.done TextBlock.subtask-title">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
@@ -934,6 +1032,11 @@
|
||||
<Setter Property="Background" Value="{DynamicResource PlannedBadgeBrush}"/>
|
||||
</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) -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -1082,6 +1185,30 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
<!-- Override Fluent's built-in accent button (SystemAccentColor = blue) at the
|
||||
ContentPresenter level so our moss tokens win across rest/hover/pressed. -->
|
||||
<Style Selector="Button.accent /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentDimBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.accent:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.accent:pressed /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||
</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 -->
|
||||
@@ -1102,4 +1229,30 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- MISSION CONTROL PANE STATUS TINTING -->
|
||||
<!-- Base neutral grey; tints layer on by status. -->
|
||||
<!-- Running / idle / queued: no class → fall through to base. -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Border.monitor-pane">
|
||||
<Setter Property="Background" Value="{DynamicResource SurfaceBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.monitor-pane.mon-done">
|
||||
<Setter Property="Background" Value="{DynamicResource DoneTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource DoneTintBorderBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.monitor-pane.mon-review">
|
||||
<Setter Property="Background" Value="{DynamicResource DoneTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource DoneTintBorderBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.monitor-pane.mon-roadblock">
|
||||
<Setter Property="Background" Value="{DynamicResource RoadblockTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource RoadblockTintBorderBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.monitor-pane.mon-failed">
|
||||
<Setter Property="Background" Value="{DynamicResource ErrorTintBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ErrorTintBorderBrush}" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
|
||||
@@ -99,6 +99,17 @@
|
||||
<SolidColorBrush x:Key="QueuedTintBorderBrush" Color="#4C8B9D7A" />
|
||||
<SolidColorBrush x:Key="DoneTintBrush" Color="#1F6FA86B" />
|
||||
<SolidColorBrush x:Key="DoneTintBorderBrush" Color="#4C6FA86B" />
|
||||
<SolidColorBrush x:Key="RoadblockTintBrush" Color="#1FD4A574" />
|
||||
<SolidColorBrush x:Key="RoadblockTintBorderBrush" Color="#4CD4A574" />
|
||||
|
||||
<!-- Merge editor (3-pane conflict resolver) block tints -->
|
||||
<SolidColorBrush x:Key="MergeOursTintBrush" Color="#1F7C9166" /> <!-- ours side (moss) -->
|
||||
<SolidColorBrush x:Key="MergeTheirsTintBrush" Color="#1FD4A574" /> <!-- theirs side (amber) -->
|
||||
<SolidColorBrush x:Key="MergeConflictTintBrush" Color="#28C87060" /> <!-- unresolved conflict (blood) -->
|
||||
<SolidColorBrush x:Key="MergeConflictEdgeBrush" Color="#80C87060" /> <!-- unresolved conflict gutter edge / map tick -->
|
||||
<SolidColorBrush x:Key="MergeResolvedTintBrush" Color="#206FA86B" /> <!-- resolved conflict (green) -->
|
||||
<SolidColorBrush x:Key="MergeResolvedEdgeBrush" Color="#806FA86B" /> <!-- resolved conflict map tick -->
|
||||
<SolidColorBrush x:Key="AmberBrush" Color="#FFD4A574" /> <!-- solid amber (theirs label) -->
|
||||
|
||||
<!-- Window-body gradient layers (apply as LinearGradientBrush in the main content Border) -->
|
||||
<LinearGradientBrush x:Key="DesktopBackgroundBrush" StartPoint="0%,0%" EndPoint="0%,100%">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Single seam for opening modal dialogs. Replaces the per-modal <c>Show*Modal</c>
|
||||
/// Func callbacks that were previously wired separately on the shell and the lists
|
||||
/// island (and the Confirm/Error dialogs duplicated in both code-behinds). The view
|
||||
/// layer supplies the implementation (<see cref="ClaudeDo.Ui.Views.WindowDialogService"/>);
|
||||
/// callers build + initialize the VM and hand it here to be shown.
|
||||
/// </summary>
|
||||
public interface IDialogService
|
||||
{
|
||||
Task ShowAboutAsync(AboutModalViewModel vm);
|
||||
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
||||
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
||||
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
||||
Task ShowRepoImportAsync(RepoImportModalViewModel 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 ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
||||
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
||||
|
||||
/// <summary>Modal yes/no confirmation. Returns true only when confirmed.</summary>
|
||||
Task<bool> ConfirmAsync(string message);
|
||||
|
||||
/// <summary>Modal error notice with a single dismiss button.</summary>
|
||||
Task ShowErrorAsync(string message);
|
||||
|
||||
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Single entry point for handing a conflicting merge to the in-app 3-pane resolver.
|
||||
/// Replaces the per-VM <c>RequestConflictResolution</c> Func seams that used to be
|
||||
/// hand-threaded shell → details → merge-section → diff → merge-modal. The shell wires
|
||||
/// <see cref="MergeCoordinator.Handler"/> once at composition; invokers depend only on
|
||||
/// this interface (injected via DI).
|
||||
/// </summary>
|
||||
public interface IMergeCoordinator
|
||||
{
|
||||
Task ResolveConflictAsync(string taskId, string targetBranch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DI singleton holding the resolver entry. The holder breaks the shell↔island construction
|
||||
/// cycle: islands depend on the interface, the shell sets <see cref="Handler"/> after it is built.
|
||||
/// </summary>
|
||||
public sealed class MergeCoordinator : IMergeCoordinator
|
||||
{
|
||||
/// Set once at composition to the shell's resolver entry. Null (headless/tests) ⇒ no-op.
|
||||
public Func<string, string, Task>? Handler { get; set; }
|
||||
|
||||
public Task ResolveConflictAsync(string taskId, string targetBranch) =>
|
||||
Handler?.Invoke(taskId, targetBranch) ?? Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed record OnlineLoginResult(bool Success, string? RefreshToken, string? Error, string? Warning = null);
|
||||
|
||||
public interface IOnlineLoginService
|
||||
{
|
||||
Task<OnlineLoginResult> LoginAsync(
|
||||
string authority, string clientId, string scope, string redirectUri,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace ClaudeDo.Ui.Services;
|
||||
public interface IWorkerClient : INotifyPropertyChanged
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
bool IsReconnecting { get; }
|
||||
|
||||
event Action<string, string, DateTime>? TaskStartedEvent;
|
||||
event Action<string, string, string, DateTime>? TaskFinishedEvent;
|
||||
@@ -17,6 +18,12 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
event Action<string>? WorktreeUpdatedEvent;
|
||||
event Action<string>? ListUpdatedEvent;
|
||||
event Action<string, string>? TaskMessageEvent;
|
||||
event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||
|
||||
/// <summary>A running task raised a question via AskUser: (taskId, questionId, question).</summary>
|
||||
event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
||||
event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
|
||||
event Action? PrepStartedEvent;
|
||||
event Action<string>? PrepLineEvent;
|
||||
@@ -28,14 +35,30 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
event Action<string>? PlanningMergeAbortedEvent;
|
||||
event Action<string>? PlanningCompletedEvent;
|
||||
|
||||
event Action<PrimeFiredEvent>? PrimeFired;
|
||||
|
||||
string? LastApproveTarget { get; }
|
||||
|
||||
IReadOnlyList<ActiveTask> GetActiveTasks();
|
||||
|
||||
Task WakeQueueAsync();
|
||||
Task RunNowAsync(string taskId);
|
||||
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
||||
/// <summary>Answer a question a running task raised via AskUser.</summary>
|
||||
Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer);
|
||||
/// <summary>The question a running task is currently blocked on, if any (for re-attach).</summary>
|
||||
Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId);
|
||||
Task ResetTaskAsync(string taskId);
|
||||
Task CancelTaskAsync(string taskId);
|
||||
Task<List<AgentInfo>> GetAgentsAsync();
|
||||
Task RefreshAgentsAsync();
|
||||
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
||||
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
||||
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<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
||||
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
||||
@@ -46,12 +69,30 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
|
||||
// ── Conflict resolution (worker hub side implemented by Layer C) ──
|
||||
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||
Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId);
|
||||
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||
Task AbortMergeAsync(string taskId);
|
||||
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
||||
Task AbortConflictMergeAsync(string taskId);
|
||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||
// Picks up a task's Claude session in a real terminal window (--resume).
|
||||
Task ResumeTaskInTerminalAsync(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>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<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
||||
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
||||
@@ -71,9 +112,29 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
event Action<string, bool, string?>? RefineFinishedEvent;
|
||||
Task ClearMyDayAsync();
|
||||
Task<AppSettingsDto?> GetAppSettingsAsync();
|
||||
Task UpdateAppSettingsAsync(AppSettingsDto dto);
|
||||
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
||||
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
||||
Task UpdateDailyNoteAsync(string id, string text);
|
||||
Task DeleteDailyNoteAsync(string id);
|
||||
Task<string> GetLastPrepLogAsync();
|
||||
Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync();
|
||||
|
||||
Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync();
|
||||
Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto);
|
||||
Task DeletePrimeScheduleAsync(Guid id);
|
||||
|
||||
Task UpdateListAsync(UpdateListDto dto);
|
||||
Task UpdateListConfigAsync(UpdateListConfigDto dto);
|
||||
|
||||
Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null);
|
||||
Task<WorktreeResetDto?> ResetAllWorktreesAsync();
|
||||
Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId);
|
||||
Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState);
|
||||
Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId);
|
||||
|
||||
Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync();
|
||||
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
||||
Task SetOnlineInboxAuthAsync(string refreshToken);
|
||||
Task ClearOnlineInboxAuthAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Duende.IdentityModel.OidcClient;
|
||||
using Duende.IdentityModel.OidcClient.Browser;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed class OnlineLoginService : IOnlineLoginService
|
||||
{
|
||||
public async Task<OnlineLoginResult> LoginAsync(
|
||||
string authority, string clientId, string scope, string redirectUri,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var browser = new LoopbackBrowser(redirectUri);
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = authority,
|
||||
ClientId = clientId,
|
||||
Scope = scope,
|
||||
RedirectUri = redirectUri,
|
||||
Browser = browser,
|
||||
};
|
||||
|
||||
var client = new OidcClient(options);
|
||||
var result = await client.LoginAsync(new LoginRequest(), ct);
|
||||
|
||||
if (result.IsError)
|
||||
return new OnlineLoginResult(false, null, result.Error);
|
||||
|
||||
if (string.IsNullOrEmpty(result.RefreshToken))
|
||||
return new OnlineLoginResult(false, null,
|
||||
"No refresh token returned. Ensure 'offline_access' is in scope and the client allows it.");
|
||||
|
||||
// Early heads-up: if the access token lacks the "user" project role the server will
|
||||
// reject sync with a 401. Login still succeeds; surface this as a warning, not an error.
|
||||
var warning = ZitadelTokenInspector.HasUserRole(result.AccessToken)
|
||||
? null
|
||||
: "missing-user-role";
|
||||
|
||||
return new OnlineLoginResult(true, result.RefreshToken, null, warning);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new OnlineLoginResult(false, null, "Login cancelled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new OnlineLoginResult(false, null, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IBrowser implementation: opens the system browser and captures the authorization
|
||||
/// response via a loopback HttpListener on the redirect URI's host/port.
|
||||
/// </summary>
|
||||
sealed class LoopbackBrowser : IBrowser
|
||||
{
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(3);
|
||||
private readonly string _redirectUri;
|
||||
|
||||
public LoopbackBrowser(string redirectUri) => _redirectUri = redirectUri;
|
||||
|
||||
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken ct = default)
|
||||
{
|
||||
// Derive the listener prefix from the redirect URI
|
||||
var uri = new Uri(_redirectUri);
|
||||
var prefix = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
|
||||
|
||||
using var listener = new HttpListener();
|
||||
listener.Prefixes.Add(prefix);
|
||||
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.UnknownError,
|
||||
Error = $"Could not start loopback listener on {prefix}: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(options.StartUrl) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.UnknownError,
|
||||
Error = $"Could not open browser: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(Timeout);
|
||||
|
||||
try
|
||||
{
|
||||
var context = await listener.GetContextAsync().WaitAsync(cts.Token);
|
||||
|
||||
var responseBody = Encoding.UTF8.GetBytes(
|
||||
"<html><body style=\"font-family:sans-serif;background:#0D1311;color:#E4EBE4;padding:40px\">" +
|
||||
"<h2>Login successful</h2><p>You may close this tab.</p></body></html>");
|
||||
|
||||
context.Response.ContentLength64 = responseBody.Length;
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
await context.Response.OutputStream.WriteAsync(responseBody, cts.Token);
|
||||
context.Response.OutputStream.Close();
|
||||
|
||||
// rawUrl already includes the redirect path (e.g. "/callback?code=..."),
|
||||
// so build the full URL from the scheme://host:port base — NOT the full
|
||||
// redirect URI, or the path would be doubled (".../callback/callback").
|
||||
var rawUrl = context.Request.RawUrl ?? "";
|
||||
var fullUri = prefix.TrimEnd('/') + rawUrl;
|
||||
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Success,
|
||||
Response = fullUri
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Timeout,
|
||||
Error = "Login timed out waiting for browser callback."
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user