refactor(worker): remove MessageParser (replaced by StreamAnalyzer)

This commit is contained in:
Mika Kuns
2026-04-14 14:12:21 +02:00
parent 03728c8e4a
commit c1c4c75979
26 changed files with 3978 additions and 88 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)"
]
}
}

View File

@@ -0,0 +1,106 @@
<h2>Green-Teal Variations</h2>
<p class="subtitle">Steel Teal shifted greener. Pick the one that feels right.</p>
<div class="cards">
<div class="card" data-choice="forest-teal" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#3d9474;display:inline-block;margin-top:4px;"></span>
<span style="color:#6bb89e; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(61,148,116,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #3d9474;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="#3d9474" stroke-width="2" fill="none"/></svg>
</div>
<div>
<div style="color:#5a6578;font-weight:500;text-decoration:line-through;">Setup CI</div>
<div style="font-size:11px;color:#475569;">Done</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#3d9474;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Forest Teal</h3>
<p>Accent: <code>#3d9474</code>. Distinctly greener, still muted. Earthy.</p>
</div>
</div>
<div class="card" data-choice="jade" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#4a9880;display:inline-block;margin-top:4px;"></span>
<span style="color:#72baa4; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(74,152,128,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #4a9880;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="#4a9880" stroke-width="2" fill="none"/></svg>
</div>
<div>
<div style="color:#5a6578;font-weight:500;text-decoration:line-through;">Setup CI</div>
<div style="font-size:11px;color:#475569;">Done</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#4a9880;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Jade</h3>
<p>Accent: <code>#4a9880</code>. Balanced green-teal midpoint. Calm but not cold.</p>
</div>
</div>
<div class="card" data-choice="sage" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#5a9a7a;display:inline-block;margin-top:4px;"></span>
<span style="color:#82bc9e; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(90,154,122,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #5a9a7a;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="#5a9a7a" stroke-width="2" fill="none"/></svg>
</div>
<div>
<div style="color:#5a6578;font-weight:500;text-decoration:line-through;">Setup CI</div>
<div style="font-size:11px;color:#475569;">Done</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#5a9a7a;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Sage</h3>
<p>Accent: <code>#5a9a7a</code>. Most green of the three. Softer, natural tone.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,107 @@
<h2>Accent Color: Which tone?</h2>
<p class="subtitle">You want something dimmer than the indigo (#6366f1) I showed. Here are darker, more muted options — each shown on a task list mockup.</p>
<div class="cards">
<div class="card" data-choice="slate-blue" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<!-- Sidebar slice -->
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#4b5ea8;display:inline-block;margin-top:4px;"></span>
<span style="color:#8b9dd4; font-weight:500;">My Project</span>
</div>
<!-- Task row -->
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(75,94,168,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<!-- Add task -->
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#4b5ea8;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Slate Blue</h3>
<p>Muted blue-gray. Accent: <code>#4b5ea8</code>. Very subdued, professional. Close to VS Code's dark theme feel.</p>
</div>
</div>
<div class="card" data-choice="dim-violet" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#7c6aad;display:inline-block;margin-top:4px;"></span>
<span style="color:#a899cc; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(124,106,173,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#7c6aad;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Dim Violet</h3>
<p>Muted purple. Accent: <code>#7c6aad</code>. Slightly warmer, still understated. Has a subtle "Claude" vibe.</p>
</div>
</div>
<div class="card" data-choice="steel-teal" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#4a8c8c;display:inline-block;margin-top:4px;"></span>
<span style="color:#7fb8b8; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(74,140,140,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#4a8c8c;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Steel Teal</h3>
<p>Muted teal-green. Accent: <code>#4a8c8c</code>. Cool and calm. Distinct from typical blue-heavy dark UIs.</p>
</div>
</div>
<div class="card" data-choice="charcoal-blue" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 16px; background: #0f0f1e;">
<div style="font-family: system-ui; font-size: 13px;">
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<span style="width:8px;height:8px;border-radius:50%;background:#5571a1;display:inline-block;margin-top:4px;"></span>
<span style="color:#8ba4c8; font-weight:500;">My Project</span>
</div>
<div style="padding: 10px 14px; display:flex; align-items:center; gap:10px; border-radius:8px; background:rgba(85,113,161,0.1); margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<div>
<div style="color:#c8d0e0;font-weight:500;">Fix login bug</div>
<div style="font-size:11px;color:#5a6578;">agent</div>
</div>
</div>
<div style="padding:10px 14px;border:1px dashed #3a4560;border-radius:8px;color:#5a6578;display:flex;align-items:center;gap:8px;font-size:13px;">
<span style="color:#5571a1;">+</span> Add a task...
</div>
</div>
</div>
<div class="card-body">
<h3>Charcoal Blue</h3>
<p>Desaturated steel blue. Accent: <code>#5571a1</code>. Very close to Microsoft To Do's dark mode accent but dimmer.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,169 @@
<h2>Layout & Visual Design Direction</h2>
<p class="subtitle">Your current UI has button toolbars and minimal spacing. Which direction should we take?</p>
<div class="split">
<div class="mockup">
<div class="mockup-header">Current: Toolbar Style</div>
<div class="mockup-body" style="font-family: system-ui; font-size: 13px;">
<div style="display: flex; height: 340px;">
<!-- Lists -->
<div style="width: 160px; border-right: 1px solid #444; display:flex; flex-direction:column;">
<div style="padding: 6px 8px; font-weight: bold; font-size: 13px;">Lists</div>
<div style="flex:1; padding: 4px;">
<div style="padding: 6px 8px; background: #3a3a5c; border-radius: 3px; margin-bottom: 2px;">My Project</div>
<div style="padding: 6px 8px;">Backend Work</div>
<div style="padding: 6px 8px;">UI Polish</div>
</div>
<div style="padding: 6px; border-top: 1px solid #444; display:flex; gap: 4px;">
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">+</button>
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">E</button>
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">-</button>
</div>
</div>
<!-- Tasks -->
<div style="flex: 1; display:flex; flex-direction:column;">
<div style="padding: 6px 8px; font-weight: bold; font-size: 13px;">Tasks</div>
<div style="flex:1; padding: 4px;">
<div style="padding: 6px; display:flex; justify-content: space-between; align-items: center; margin-bottom: 2px;">
<div>
<div style="font-weight: 600;">Fix login bug</div>
<div style="font-size: 10px; color: #888;">agent</div>
</div>
<div style="display:flex; gap: 4px; align-items:center;">
<span style="background: #e67e22; color: white; padding: 1px 6px; border-radius: 3px; font-size: 11px;">Running</span>
<button style="padding: 1px 8px; font-size: 11px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">Run</button>
</div>
</div>
<div style="padding: 6px; display:flex; justify-content: space-between; align-items: center;">
<div><div style="font-weight: 600;">Add dark mode</div></div>
<div style="display:flex; gap: 4px; align-items:center;">
<span style="background: #666; color: white; padding: 1px 6px; border-radius: 3px; font-size: 11px;">Manual</span>
<button style="padding: 1px 8px; font-size: 11px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">Run</button>
</div>
</div>
</div>
<div style="padding: 6px; border-top: 1px solid #444; display:flex; gap: 4px;">
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">+ Task</button>
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">Edit</button>
<button style="padding: 2px 10px; font-size: 12px; background: #555; color: white; border: 1px solid #666; border-radius: 3px;">Delete</button>
</div>
</div>
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">Proposed: To Do Style</div>
<div class="mockup-body" style="font-family: system-ui; font-size: 13px;">
<div style="display: flex; height: 340px;">
<!-- Lists sidebar -->
<div style="width: 160px; border-right: 1px solid #333; background: #1a1a2e; display:flex; flex-direction:column;">
<div style="padding: 14px 16px 10px; font-weight: 600; font-size: 14px; color: #94a3b8;">Lists</div>
<div style="flex:1; padding: 4px 8px;">
<div style="padding: 10px 12px; background: rgba(99,102,241,0.15); border-radius: 8px; margin-bottom: 4px; color: #a5b4fc; font-weight: 500; display:flex; align-items:center; gap:8px;">
<span style="width:8px;height:8px;border-radius:50%;background:#6366f1;display:inline-block;"></span>
My Project
</div>
<div style="padding: 10px 12px; border-radius: 8px; display:flex; align-items:center; gap:8px; color: #94a3b8;">
<span style="width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block;"></span>
Backend Work
</div>
<div style="padding: 10px 12px; border-radius: 8px; display:flex; align-items:center; gap:8px; color: #94a3b8;">
<span style="width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block;"></span>
UI Polish
</div>
</div>
<div style="padding: 8px 12px; border-top: 1px solid #333;">
<div style="padding: 8px 12px; color: #6366f1; cursor:pointer; border-radius: 6px; display:flex; align-items:center; gap: 6px; font-size: 13px;">
<span style="font-size: 16px;">+</span> New List
</div>
</div>
</div>
<!-- Tasks -->
<div style="flex: 1; display:flex; flex-direction:column; background: #16162a;">
<div style="padding: 14px 16px 10px; font-weight: 600; font-size: 16px; color: #e2e8f0;">My Project</div>
<div style="flex:1; padding: 0 8px;">
<div style="padding: 10px 12px; display:flex; align-items: center; gap: 10px; margin-bottom: 2px; border-radius: 8px; background: rgba(99,102,241,0.08);">
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #e67e22; display:flex; align-items:center; justify-content:center; flex-shrink:0;">
<div style="width:8px;height:8px;border-radius:50%;background:#e67e22;"></div>
</div>
<div style="flex:1;">
<div style="font-weight: 500; color: #e2e8f0;">Fix login bug</div>
<div style="font-size: 11px; color: #64748b; margin-top: 2px;">agent &middot; Running</div>
</div>
</div>
<div style="padding: 10px 12px; display:flex; align-items: center; gap: 10px; border-radius: 8px;">
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #475569; flex-shrink:0;"></div>
<div style="flex:1;">
<div style="font-weight: 500; color: #e2e8f0;">Add dark mode</div>
<div style="font-size: 11px; color: #64748b;">manual</div>
</div>
</div>
<div style="padding: 10px 12px; display:flex; align-items: center; gap: 10px; border-radius: 8px; opacity: 0.5;">
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #22c55e; display:flex; align-items:center; justify-content:center; flex-shrink:0;">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="#22c55e" stroke-width="2" fill="none"/></svg>
</div>
<div style="flex:1;">
<div style="font-weight: 500; color: #64748b; text-decoration: line-through;">Setup CI pipeline</div>
<div style="font-size: 11px; color: #475569;">agent &middot; Done</div>
</div>
</div>
</div>
<!-- Inline add -->
<div style="padding: 8px 12px; border-top: 1px solid #333;">
<div style="padding: 10px 14px; border: 1px dashed #475569; border-radius: 8px; color: #64748b; display:flex; align-items: center; gap: 8px; font-size: 13px;">
<span style="font-size: 16px; color: #6366f1;">+</span> Add a task...
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 32px;">Key Changes in the Proposed Design</h3>
<div class="options" data-multiselect>
<div class="option" data-choice="checkbox" onclick="toggleSelect(this)">
<div class="letter">1</div>
<div class="content">
<h3>Circular Checkboxes</h3>
<p>Replace status badges with circular checkboxes on the left. Border color reflects status (orange=running, green=done, gray=manual). Click to toggle done.</p>
</div>
</div>
<div class="option" data-choice="inline-add" onclick="toggleSelect(this)">
<div class="letter">2</div>
<div class="content">
<h3>Inline "Add a task" Input</h3>
<p>Dashed border text field pinned at the bottom of the task list. Always visible. Enter to create, Escape to cancel.</p>
</div>
</div>
<div class="option" data-choice="list-header" onclick="toggleSelect(this)">
<div class="letter">3</div>
<div class="content">
<h3>List Name as Tasks Header</h3>
<p>Replace generic "Tasks" header with the selected list name in larger text. Matches To Do's pattern.</p>
</div>
</div>
<div class="option" data-choice="sidebar-polish" onclick="toggleSelect(this)">
<div class="letter">4</div>
<div class="content">
<h3>Sidebar Polish</h3>
<p>Colored dots per list, subtle highlight on selected, "+ New List" link at bottom instead of +/E/- buttons.</p>
</div>
</div>
<div class="option" data-choice="remove-buttons" onclick="toggleSelect(this)">
<div class="letter">5</div>
<div class="content">
<h3>Remove Button Toolbars</h3>
<p>Eliminate the bottom button bars from both panes. All actions via context menu, keyboard shortcuts, or inline controls.</p>
</div>
</div>
<div class="option" data-choice="done-style" onclick="toggleSelect(this)">
<div class="letter">6</div>
<div class="content">
<h3>Completed Task Styling</h3>
<p>Done tasks get strikethrough text, reduced opacity, green checkmark. Keeps them visible but visually subordinate.</p>
</div>
</div>
</div>
<p class="subtitle" style="margin-top: 16px;">This is multi-select — pick all the changes you'd like to include. I recommend all 6.</p>

View File

@@ -0,0 +1,61 @@
<h2>Task Creation: How should adding tasks work?</h2>
<p class="subtitle">Microsoft To Do uses an inline text field at the bottom of the task list. Currently ClaudeDo opens a modal dialog. Which approach fits best?</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Inline Add (To Do style)</h3>
<p>A text field always visible at the bottom of the task list. Press <strong>Enter</strong> to create a quick task with just a title. Tab or click to expand for more fields (description, tags, status).</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul>
<li>Fastest for rapid task entry</li>
<li>Keyboard-driven — never leave the list</li>
<li>Feels natural and lightweight</li>
</ul></div>
<div class="cons"><h4>Cons</h4><ul>
<li>Limited space for advanced fields</li>
<li>Need separate flow for setting tags/status on creation</li>
</ul></div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Inline Add + Detail Pane Editing</h3>
<p>Same inline text field for quick creation. After pressing Enter, the new task is selected and the <strong>detail pane on the right</strong> becomes editable — add description, tags, commit type there. Like To Do's "click task → edit in sidebar" pattern.</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul>
<li>Quick entry AND full editing without modals</li>
<li>Uses existing detail pane real estate</li>
<li>Closest to Microsoft To Do's actual flow</li>
</ul></div>
<div class="cons"><h4>Cons</h4><ul>
<li>Detail pane needs to become editable (currently read-only)</li>
<li>More complex state management</li>
</ul></div>
</div>
</div>
</div>
<div class="option" data-choice="c" onclick="toggleSelect(this)">
<div class="letter">C</div>
<div class="content">
<h3>Keep Modal Dialog + Keyboard Shortcut</h3>
<p>Keep the existing modal editor but add <strong>Ctrl+N</strong> / <strong>Enter</strong> shortcut to open it instantly. Add keyboard navigation within the dialog (Tab between fields, Enter to save).</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul>
<li>Minimal code changes</li>
<li>All fields visible at once</li>
<li>Modal keeps focus clear</li>
</ul></div>
<div class="cons"><h4>Cons</h4><ul>
<li>Still interrupts flow with a window</li>
<li>Feels heavier than To Do</li>
</ul></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1776154800894}

View File

@@ -0,0 +1 @@
1955

View File

@@ -0,0 +1,79 @@
<h2>Island-Farben: weniger grün</h2>
<p class="subtitle">Gleiche Struktur, neutralere Grautöne mit nur einem Hauch Grün</p>
<div class="cards">
<div class="card" data-choice="neutral-slate" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 10px; background: #1b1e23;">
<div style="display: flex; gap: 8px; height: 180px; font-family: system-ui; font-size: 12px;">
<div style="flex: 1; background: #252a30; border-radius: 12px; padding: 10px;">
<div style="color: #94a3b8; font-size: 11px; margin-bottom: 8px;">Lists</div>
<div style="padding: 6px 8px; background: rgba(61,148,116,0.12); border-radius: 6px; color: #6bb89e; font-size: 12px;">My Project</div>
</div>
<div style="flex: 2; background: #252a30; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px; margin-bottom: 8px;">My Project</div>
<div style="padding: 6px; display:flex; align-items:center; gap:6px;">
<div style="width:16px;height:16px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<span style="color:#e2e8f0;">Fix login bug</span>
</div>
</div>
<div style="flex: 1.2; background: #252a30; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px;">Detail</div>
</div>
</div>
</div>
<div class="card-body">
<h3>Neutral Slate</h3>
<p>Base: <code>#1b1e23</code> · Islands: <code>#252a30</code><br>Fast kein Grün — kühl, neutral, wie VS Code Dark+</p>
</div>
</div>
<div class="card" data-choice="warm-charcoal" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 10px; background: #1c1e21;">
<div style="display: flex; gap: 8px; height: 180px; font-family: system-ui; font-size: 12px;">
<div style="flex: 1; background: #272a2e; border-radius: 12px; padding: 10px;">
<div style="color: #94a3b8; font-size: 11px; margin-bottom: 8px;">Lists</div>
<div style="padding: 6px 8px; background: rgba(61,148,116,0.12); border-radius: 6px; color: #6bb89e; font-size: 12px;">My Project</div>
</div>
<div style="flex: 2; background: #272a2e; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px; margin-bottom: 8px;">My Project</div>
<div style="padding: 6px; display:flex; align-items:center; gap:6px;">
<div style="width:16px;height:16px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<span style="color:#e2e8f0;">Fix login bug</span>
</div>
</div>
<div style="flex: 1.2; background: #272a2e; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px;">Detail</div>
</div>
</div>
</div>
<div class="card-body">
<h3>Warm Charcoal</h3>
<p>Base: <code>#1c1e21</code> · Islands: <code>#272a2e</code><br>Minimal warm, komplett neutral. Wie Rider's New UI Dark.</p>
</div>
</div>
<div class="card" data-choice="subtle-tint" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 10px; background: #1b1f22;">
<div style="display: flex; gap: 8px; height: 180px; font-family: system-ui; font-size: 12px;">
<div style="flex: 1; background: #262b2d; border-radius: 12px; padding: 10px;">
<div style="color: #94a3b8; font-size: 11px; margin-bottom: 8px;">Lists</div>
<div style="padding: 6px 8px; background: rgba(61,148,116,0.12); border-radius: 6px; color: #6bb89e; font-size: 12px;">My Project</div>
</div>
<div style="flex: 2; background: #262b2d; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px; margin-bottom: 8px;">My Project</div>
<div style="padding: 6px; display:flex; align-items:center; gap:6px;">
<div style="width:16px;height:16px;border-radius:50%;border:2px solid #475569;flex-shrink:0;"></div>
<span style="color:#e2e8f0;">Fix login bug</span>
</div>
</div>
<div style="flex: 1.2; background: #262b2d; border-radius: 12px; padding: 10px;">
<div style="color: #e2e8f0; font-size: 14px;">Detail</div>
</div>
</div>
</div>
<div class="card-body">
<h3>Subtle Tint</h3>
<p>Base: <code>#1b1f22</code> · Islands: <code>#262b2d</code><br>Ganz leichter kühler Ton — zwischen den anderen beiden</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,77 @@
<h2>Island Layout (Rider-Style)</h2>
<p class="subtitle">Dark greenish-gray base, rounded card panels floating on top</p>
<div class="split">
<div class="mockup">
<div class="mockup-header">Current: Flat columns, black gaps</div>
<div class="mockup-body" style="font-family: system-ui; font-size: 13px; background: #000; padding: 0;">
<div style="display: flex; height: 300px;">
<div style="width: 140px; background: #1a1a2e; padding: 10px;">
<div style="color: #94a3b8; font-size: 12px; margin-bottom: 8px;">Lists</div>
<div style="padding: 8px; background: rgba(61,148,116,0.15); border-radius: 4px; color: #6bb89e; font-size: 13px; margin-bottom: 4px;">My Project</div>
<div style="padding: 8px; color: #94a3b8; font-size: 13px;">Backend</div>
</div>
<div style="width: 4px; background: #000;"></div>
<div style="flex: 1; background: #16162a; padding: 10px;">
<div style="color: #e2e8f0; font-size: 15px; margin-bottom: 8px;">My Project</div>
<div style="padding: 8px; color: #e2e8f0; font-size: 13px;">Fix login bug</div>
</div>
<div style="width: 4px; background: #000;"></div>
<div style="width: 160px; background: #16162a; padding: 10px;">
<div style="color: #e2e8f0; font-size: 15px; margin-bottom: 8px;">Detail</div>
</div>
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">Proposed: Floating islands on tinted base</div>
<div class="mockup-body" style="font-family: system-ui; font-size: 13px; background: #1a2420; padding: 8px;">
<div style="display: flex; height: 300px; gap: 8px;">
<div style="width: 140px; background: #222d29; border-radius: 12px; padding: 12px;">
<div style="color: #94a3b8; font-size: 12px; margin-bottom: 10px;">Lists</div>
<div style="padding: 8px 10px; background: rgba(61,148,116,0.15); border-radius: 8px; color: #6bb89e; font-size: 13px; margin-bottom: 4px;">My Project</div>
<div style="padding: 8px 10px; color: #94a3b8; font-size: 13px; border-radius: 8px;">Backend</div>
</div>
<div style="flex: 1; background: #222d29; border-radius: 12px; padding: 12px;">
<div style="color: #e2e8f0; font-size: 15px; margin-bottom: 10px;">My Project</div>
<div style="padding: 8px 10px; display: flex; align-items: center; gap: 8px; border-radius: 8px;">
<div style="width: 18px; height: 18px; border-radius: 50%; border: 2px solid #475569; flex-shrink: 0;"></div>
<div>
<div style="color: #e2e8f0; font-size: 13px;">Fix login bug</div>
<div style="color: #5a6578; font-size: 11px;">agent · manual</div>
</div>
</div>
<div style="padding: 8px 10px; display: flex; align-items: center; gap: 8px; border-radius: 8px; opacity: 0.6;">
<div style="width: 18px; height: 18px; border-radius: 50%; border: 2px solid #3d9474; display:flex;align-items:center;justify-content:center; flex-shrink: 0;">
<svg width="10" height="10" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="#3d9474" stroke-width="2" fill="none"/></svg>
</div>
<div>
<div style="color: #5a6578; font-size: 13px; text-decoration: line-through;">Setup CI</div>
<div style="color: #475569; font-size: 11px;">done</div>
</div>
</div>
</div>
<div style="width: 160px; background: #222d29; border-radius: 12px; padding: 12px;">
<div style="color: #e2e8f0; font-size: 15px; margin-bottom: 10px;">Fix login bug</div>
<div style="color: #94a3b8; font-size: 12px;">Status</div>
<div style="color: #e2e8f0; font-size: 13px; margin-bottom: 8px;">Manual</div>
<div style="color: #94a3b8; font-size: 12px;">Tags</div>
<div style="display:flex; gap:4px; margin-top: 4px;">
<span style="background: rgba(61,148,116,0.15); color: #6bb89e; padding: 2px 8px; border-radius: 10px; font-size: 11px;">agent</span>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Die Änderungen</h3>
<ul style="color: #cbd5e1; line-height: 1.8;">
<li><strong>Window-Background:</strong> <code>#1a2420</code> — dunkles Grau mit Grünstich</li>
<li><strong>Island-Background:</strong> <code>#222d29</code> — etwas heller, ebenfalls grünlich</li>
<li><strong>Border-Radius:</strong> 12px auf allen drei Spalten</li>
<li><strong>Gap:</strong> 8px zwischen den Islands (GridSplitter entfernen, Margin nutzen)</li>
<li><strong>Padding:</strong> 8px um das gesamte Grid (Window-Rand)</li>
<li><strong>GridSplitter weg</strong> — die Islands haben feste Abstände, Resizing via Window-Größe</li>
</ul>

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1776158304159}

View File

@@ -0,0 +1 @@
3761

54
CLAUDE.md Normal file
View File

@@ -0,0 +1,54 @@
# ClaudeDo
A desktop task management app that executes tasks autonomously via Claude CLI in isolated git worktrees.
## Architecture
Two-process system communicating over SignalR (`127.0.0.1:47821`):
- **ClaudeDo.App** — Avalonia desktop entry point, DI container setup
- **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
## Tech Stack
- .NET 8.0, Avalonia 12.0.0 (Fluent theme)
- SQLite (WAL mode) via Microsoft.Data.Sqlite — raw ADO.NET, no ORM
- SignalR for real-time IPC
- CommunityToolkit.Mvvm (`[ObservableProperty]`, `[RelayCommand]`)
- Git worktrees for task isolation
## Key Paths
- DB: `~/.todo-app/todo.db`
- UI config: `~/.todo-app/ui.config.json`
- Worker config: `~/.todo-app/worker.config.json`
- Logs: `~/.todo-app/logs/`
- Worktrees: configured per worker (sibling or central strategy)
- Schema: `schema/schema.sql` (embedded resource in ClaudeDo.Data)
## Conventions
- Repository pattern — each entity has its own async repository
- All data operations are async with CancellationToken support
- Task status flow: Manual | Queued -> Running -> Done | Failed
- Worktree state flow: Active -> Merged | Discarded | Kept
- Tags "agent" and "manual" are seeded; "agent" tag marks tasks for automated queue pickup
- Commit messages use conventional format: `{commitType}(slug): title`
- Views use compiled bindings (`x:DataType`)
- ViewModels use `[ObservableProperty]` and `[RelayCommand]` source generators
## Building & Testing
```bash
dotnet build ClaudeDo.slnx
dotnet test tests/ClaudeDo.Worker.Tests
```
## Docs
- `docs/plan.md` — full architecture and design spec
- `docs/open.md` — verification checklist and improvement backlog
- `docs/improvement-plan.md` — prioritized improvement items

92
docs/improvement-plan.md Normal file
View File

@@ -0,0 +1,92 @@
# ClaudeDo — Improvement Plan (Session 2026-04-13)
Erfasst während manuellem Walkthrough der App. Priorisiert nach Schmerz/Aufwand.
---
## P1 — UX-Blocker (sollten zuerst)
### IP-1: UI ↔ Worker Auto-Reconnect
**Symptom:** Wenn UI vor Worker startet, bleibt die Verbindung tot. Manueller UI-Restart nötig.
**Soll:** SignalR-Client mit `WithAutomaticReconnect()` + Reconnect-Versuche im Hintergrund (exponential backoff). Status-Bar zeigt "verbinde…" während Retry.
**Dateien:** `src/ClaudeDo.Ui/Services/WorkerClient.cs` (oder wo `HubConnection` gebaut wird)
**Aufwand:** klein (~30 Zeilen, primär `HubConnectionBuilder`-Konfig + Reconnect-Handler)
**Risiko:** klein
### IP-2: Listen-Modus „Notes" (non-autonomous)
**Symptom:** Jede Liste ist Agent-gesteuert. Keine reine Notiz-Liste möglich.
**Soll:** Neues Feld `lists.kind` (`agent` | `notes`).
- `agent`: aktuelles Verhalten (Worker pickt Tasks)
- `notes`: Worker ignoriert die Liste komplett, UI versteckt Run-/Schedule-/Worktree-Felder, Tasks haben nur Title + Description + done-Checkbox.
**Dateien:**
- Schema: neue Spalte + Migration (siehe IP-9)
- `Data/Entities/TaskList.cs`, `Repositories/ListRepository.cs`
- `Worker/Queue/QueueService.cs` (Filter `WHERE list.kind = 'agent'`)
- UI: `ListEditorView` (Radio/ComboBox), `TaskListView` (conditional Columns), `TaskDetailView` (verstecken)
**Aufwand:** mittel (~Schema + Repo + UI an mehreren Stellen)
**Risiko:** mittel — bestehende Listen müssen Default `agent` bekommen
### IP-3: Doppelklick öffnet Edit-Dialog
**Symptom:** Edit nur über separaten Button/Menüpunkt.
**Soll:** `DoubleTapped`-Handler auf ListBox-Items (Listen-Pane) und auf TaskRows (Task-Pane) → öffnet jeweiligen Editor.
**Dateien:** `Views/MainWindow.axaml(.cs)`, `Views/TaskListView.axaml(.cs)`
**Aufwand:** klein (~1015 Zeilen pro Stelle)
**Risiko:** klein
### IP-4: Tag-Multi-Select statt Freitext
**Symptom:** Tags müssen getippt werden, keine Auto-Vervollständigung, Typos möglich.
**Soll:** Multi-Select-Control:
- Zeigt alle in DB existierenden Tags (DISTINCT aus `lists.tags` `tasks.tags`)
- Erlaubt Anlegen neuer Tags (Free-Text-Add)
- Chip/Token-Darstellung der ausgewählten Tags
**Dateien:**
- *neu* `Views/Controls/TagPicker.axaml` (wiederverwendbar)
- `ListEditorView`, `TaskEditorView` einbinden
- Repo-Methode `GetAllKnownTagsAsync()`
**Aufwand:** mittel (Custom-Control lohnt sich, da 2× verwendet)
**Risiko:** klein
### IP-5: Rechtsklick-Kontextmenü
**Symptom:** Quick-Actions nur über Buttons im Detail-Pane oder Toolbar.
**Soll:**
- **Liste:** Edit, Delete, New Task, ggf. „Mark all done" (für Notes-Listen aus IP-2)
- **Task:** Edit, Delete, Run Now, Show Diff, Merge, Cancel (je nach Status)
- Items kontext-sensitiv enabled/disabled je nach Task-Status & List-Kind
**Dateien:** `Views/MainWindow.axaml` (List-Pane), `Views/TaskListView.axaml` (Task-Pane)
**Aufwand:** kleinmittel — Avalonia `ContextMenu` + Command-Bindings
**Risiko:** klein
---
## P2 — Folge-Arbeiten (durch P1 ausgelöst)
### IP-6: Schema-Migration-Mechanismus
**Trigger:** IP-2 fügt eine Spalte zu `lists` hinzu. Aktuell `schema.sql` ist Drop-and-Create-Style.
**Soll:** Mini-Migrations-System: `migrations/0001_initial.sql`, `0002_lists_kind.sql`, … + `_schema_version` Tabelle.
**Aufwand:** kleinmittel
**Querverweis:** `open.md` Sektion 7 (Schulden-Tabelle: „Embedded schema.sql ohne Versionierung")
### IP-7: Status-Bar zeigt Reconnect-State
**Trigger:** IP-1 — User soll sehen, dass Verbindung gerade aufgebaut wird (statt nur „offline").
**Soll:** States: `connected` | `connecting` | `reconnecting` | `offline`. Farb-codiert.
**Datei:** `ViewModels/StatusBarViewModel.cs`
**Aufwand:** klein
### IP-8: Tag-Repository für `GetAllKnownTagsAsync`
**Trigger:** IP-4 braucht eine Quelle aller bekannten Tags.
**Soll:** Methode in `ListRepository`/`TaskRepository` ODER neuer `TagRepository`. SQL: `SELECT DISTINCT trim(value) FROM lists, json_each(lists.tags) UNION ...`.
**Aufwand:** klein
---
## Empfohlene Reihenfolge
1. **IP-1** (Auto-Reconnect) — sofortiger UX-Win, isoliert, klein
2. **IP-3** (Doppelklick) — trivial, sofort spürbar
3. **IP-5** (Kontextmenü) — kompakt, hebt Bedienkomfort deutlich
4. **IP-6** (Migrations) — Voraussetzung für IP-2
5. **IP-2** (Notes-Mode) — größerer Brocken, braucht Schema-Migration
6. **IP-8 → IP-4** (Tag-Repo, dann Multi-Select-Control)
7. **IP-7** (Reconnect-Status in StatusBar) — Polish nach IP-1
Block 1 (IP-1, IP-3, IP-5) ist ein realistischer Session-Block.

193
docs/open.md Normal file
View File

@@ -0,0 +1,193 @@
# ClaudeDo — Offene Punkte
Stand: 2026-04-13 nach Slice F. Branch `main` @ `48e4aab`. Alle Tests grün (38/38), Build 0 Warnings.
Dieses Dokument listet alles, was noch fehlt — gruppiert nach Aufwand/Risiko und mit konkreten Datei-Pointern, damit wir es in der IDE der Reihe nach durchgehen können.
---
## 1. Verification (vor allem anderen)
Die in `plan.md` definierten Verification-Steps sind teilweise nur durch Build/Tests abgedeckt. Diese sollten manuell einmal durchlaufen werden, BEVOR wir Polish bauen — damit wir wissen, was tatsächlich kaputt ist.
| # | Plan | Status | Was tun |
|---|------|--------|---------|
| 1 | Schema-Init | Auto verifiziert (Worker startet ohne Crash, WAL-Files entstehen) | OK |
| 1a | SignalR-Endpoint | Manuell verifiziert (HTTP 400 auf `/hub` ohne Handshake) | OK |
| 1b | Hub-Roundtrip `Ping` | **Nicht getestet** | Test-Client schreiben oder UI starten und im Log nach "pong" schauen |
| 2 | `claude --version` Preflight | **Nicht implementiert** | `Worker/Program.cs`: vor `app.Run()` einmal `claude --version` shellen und bei Exit≠0 abbrechen |
| 3 | Smoke-Spawn (`claude -p` mit Prompt "ping") | **Nicht getestet** | Integrationstest schreiben oder einmal manuell laufen lassen |
| 4 | E2E Happy Path (Non-Worktree) | **Nicht getestet** | UI starten → Liste "Test" anlegen → Task mit Tag `agent` + Status `queued` + Description "Schreibe ein Haiku über Intralogistik" → Run abwarten → Result prüfen |
| 5 | Worktree Happy Path | **Nicht getestet** | Manueller Test mit echtem Repo (z.B. einem temp-Repo) |
| 6 | No-Changes-Run | **Nicht getestet** | Prompt der nichts ändert → `head_commit` bleibt NULL |
| 7 | Kein Git-Repo | **Nicht getestet** | working_dir auf `C:\Temp` → Task `failed`, keine `worktrees`-Row |
| 8 | Merge-UI | **Nicht getestet** (UI ruft `GitService.MergeFfOnlyAsync`, aber nie ausgeführt) | Manuell |
| 9 | Override-Parallelität | Tests vorhanden für Slot-Logik, **End-to-End nicht** | UI: zwei Tasks queuen, `Run Now` auf der zweiten → beide laufen parallel |
| 10 | Schedule | Logik per Test abgedeckt, **End-to-End nicht** | Task mit `scheduled_for = now+2min` |
| 11 | Worker-Offline-Erkennung | UI hat Status-Bar, aber **nicht visuell verifiziert** | Worker killen, schauen ob Status auf "offline" wechselt |
| 12 | Live-Stream | **Nicht getestet** | Während Run TaskDetail öffnen, beobachten ob ndjson-Zeilen erscheinen |
| 13 | Wake-up (UI ruft `WakeQueue` nach Anlage) | Implementiert in `TaskListViewModel`, **nicht visuell verifiziert** | Tasks nach Anlage in <1s gepickert |
**Vorschlag:** Wir machen einmal Step 4 (Haiku-Happy-Path) gemeinsam — wenn das läuft, ist die ganze Pipeline lebendig.
---
## 2. UI-Polish (kritisch für Benutzbarkeit)
Im aktuellen Stand kompiliert die UI, aber mehrere Stellen sind als `// TODO` markiert. Reihenfolge nach Schmerz:
### 2.1 Folder-Picker für `Working Directory`
- **Datei:** `src/ClaudeDo.Ui/Views/ListEditorView.axaml` + `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs`
- **Aktuell:** plain `TextBox` — Pfad muss getippt werden.
- **Soll:** Button "…" daneben → öffnet `IStorageProvider.OpenFolderPickerAsync`, schreibt Pfad ins Feld.
- **Aufwand:** klein, ~30 Zeilen.
### 2.2 Delete-Confirmation
- **Dateien:** `MainWindowViewModel.DeleteList`, `TaskListViewModel.DeleteTask`
- **Aktuell:** löscht direkt ohne Rückfrage. Datenverlust-Risiko.
- **Soll:** Mini-Dialog "Wirklich löschen?" mit Ja/Nein.
- **Aufwand:** klein, generisches `ConfirmDialog` lohnt sich (1× bauen, mehrfach nutzen).
### 2.3 Markdown-Rendering für Result + Description
- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml`
- **Aktuell:** `TextBox IsReadOnly="True"` mit Plaintext.
- **Soll:** `Markdown.Avalonia` Package einbinden und auf `MarkdownScrollViewer` umstellen.
- **Aufwand:** mittel — Package + ein paar XAML-Anpassungen. Theme-Integration kann nerven.
### 2.4 Live-Log Auto-Scroll
- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` (oder im VM)
- **Aktuell:** ndjson-Zeilen werden angehängt, aber Scrollposition bleibt stehen.
- **Soll:** Bei jeder neuen Zeile `ScrollViewer.ScrollToEnd()` solange User nicht manuell hochgescrollt hat (Sticky-Bottom-Pattern).
- **Aufwand:** klein, ein attached behavior reicht.
### 2.5 Diff-Viewer
- **Datei:** `TaskDetailViewModel.ShowDiffAsync`
- **Aktuell:** `Process.Start("cmd", "/k git diff …")` — separates Konsolenfenster, hässlich.
- **Soll:** entweder unified-diff inline anzeigen (`git diff` Output in `TextBox` mit Mono-Font + Color für +/-) oder einen externen Diff-Tool-Hook (`git difftool`).
- **Aufwand:** mittel. MVP: einfach nur den Diff-Output in einem Modal.
### 2.6 Status-Bar Active-Tasks Live-Update
- **Datei:** `StatusBarViewModel`
- **Risiko:** das Slot-State-Update kommt vom WorkerClient, aber `RunNowCommand.NotifyCanExecuteChanged` triggert nicht pro Item bei `IsConnected`-Wechsel (vom Slice-F-Agent dokumentiert).
- **Soll:** Über `WeakReferenceMessenger` (CommunityToolkit.Mvvm) eine Connection-Change-Message verteilen, an die alle `TaskItemViewModel` lauschen.
- **Aufwand:** klein, aber muss sauber gemacht werden.
### 2.7 Settings-Dialog
- **Datei:** *neu*`Views/SettingsDialog.axaml` + VM
- **Aktuell:** `~/.todo-app/ui.config.json` muss von Hand editiert werden.
- **Soll:** Dialog mit Feldern: DB-Pfad, SignalR-Port, Default-Tags. Persistiert zurück in JSON.
- **Aufwand:** mittel. Achtung: Port-Wechsel braucht Worker-Restart.
---
## 3. Worker-Robustheit
### 3.1 CLI-Preflight beim Worker-Start
- **Datei:** `src/ClaudeDo.Worker/Program.cs`
- **Soll:** vor `app.Run()` `claude --version` ausführen; bei Fehler `app.Logger.LogCritical` + `Environment.Exit(1)`.
- **Aufwand:** klein, ~20 Zeilen. Liefert Verification Step 2.
### 3.2 Worktree-Cleanup beim Anlege-Failed
- **Datei:** `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
- **Aktuell:** Wenn `WorktreeAddAsync` zwischen `CreateAsync`-Schritten failt (z.B. Branch existiert schon), bleibt evtl. ein halbangelegter Worktree-Dir auf der Platte.
- **Soll:** try/finally — bei Fehler `git worktree remove --force` als Best-Effort-Cleanup.
- **Aufwand:** klein.
### 3.3 Logging über `Microsoft.Extensions.Logging` strukturieren
- **Datei:** alle Worker-Komponenten
- **Aktuell:** ILogger wird benutzt, aber kein File-Sink konfiguriert.
- **Soll:** Optional Serilog oder einfach `AddFile` (Karambolage.Extensions.Logging.File) — Service-Modus braucht persistente Logs außerhalb der Console.
- **Aufwand:** klein.
### 3.4 Tag-Negation / Exclusion (Plan-TODO)
- **Plan-Sektion:** "Tag-Modell"
- **Aktuell:** Tags sind rein additiv (`list_tags task_tags`).
- **Soll:** Mechanismus, um auf Task-Ebene einen List-Tag auszuschließen. Z.B. neue Tabelle `task_tag_exclusions` ODER ein Prefix `!tag` im task_tags-Eintrag.
- **Aufwand:** mittel — Schema + Repo + Tests + UI.
---
## 4. Service-Deployment (Plan-Sektion „Worker als Windows-Service")
### 4.1 Windows-Service-Hosting in Code
- **Datei:** `src/ClaudeDo.Worker/Program.cs`
- **Pakete:** `Microsoft.Extensions.Hosting.WindowsServices`
- **Soll:**
```csharp
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
builder.Logging.AddEventLog(...);
```
- **Aufwand:** klein.
### 4.2 Pfad-Auflösung absolut machen
- Bereits in `WorkerConfig.Load` per `Paths.Expand` gemacht — verifizieren, dass auch `cfg.ClaudeBin` ggf. in Service-PATH gefunden wird.
### 4.3 Install-Skripte / Doku
- **Datei:** *neu* — `docs/install-service.md` oder `scripts/install-service.cmd`
- **Inhalt:** `dotnet publish` + `sc.exe create` + `sc.exe failure` + Hinweis auf `obj=` (User-Account) wegen Claude-CLI-Session.
- **Aufwand:** klein.
### 4.4 (später) Installer-Projekt
- WiX/MSIX, registriert Service + UI-Shortcut. Plan-Sektion „Offene Punkte".
---
## 5. Tests / CI
### 5.1 GitHub-Actions / Gitea-Actions Pipeline
- **Datei:** *neu* — `.gitea/workflows/ci.yml` (oder `.github/workflows/ci.yml`)
- **Inhalt:** `dotnet restore` → `dotnet build --no-restore` → `dotnet test --no-build`. Auf Push + PR.
- **Aufwand:** klein.
### 5.2 Echter SignalR-Roundtrip-Test
- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Hub/WorkerHubTests.cs`
- **Soll:** mit `WebApplicationFactory` + `HubConnectionBuilder` testen, dass `Ping`, `GetActive`, `RunNow`-Throw-Verhalten korrekt sind. Plan-Verification 1b + 9.
- **Aufwand:** mittel.
### 5.3 Smoke-Test gegen echten `claude`
- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Runner/ClaudeProcessSmokeTest.cs`
- **Soll:** Real-CLI-Test, der mit `[Fact(Skip="..."]` ausgegraut bleibt und nur lokal aktiviert wird, wenn `CLAUDE_AUTHENTICATED=1` Env-Var gesetzt ist.
- **Aufwand:** klein.
---
## 6. Dokumentation
### 6.1 README.md
- Komplett fehlt. Mind. 1× kurz: was ist es, wie starten (Worker + UI), wo Config.
- **Aufwand:** klein.
### 6.2 `docs/architecture.md`
- In `plan.md` schon teilweise enthalten — kann entweder konsolidiert oder explizit ausgegliedert werden.
### 6.3 ADRs für die getroffenen Entscheidungen
- Z.B. „SignalR vs. SQLite-Polling für IPC", „Worktree pro Task", „SignalR über Loopback ohne Auth".
- **Aufwand:** klein, hilfreich für später.
---
## 7. Bekannte Code-Schulden / Smells
| Stelle | Issue |
|---|---|
| `WorkerHub.GetActive` returnt `IReadOnlyList<object>` mit anonymen Typen | Sollte ein expliziter DTO sein (`ActiveTaskDto`), den Worker UND Ui teilen. Aktuell duplizieren beide das Schema. |
| `TaskRunner` führt eine `if (list.WorkingDir != null)` Verzweigung mitten in der Methode | Strategy-Pattern (`IRunStrategy`: SandboxStrategy, WorktreeStrategy) wenn die Methode wächst. Aktuell noch klein genug. |
| `App.Services` als public static `ServiceProvider` | Service-Locator-Antipattern. Toleriert, weil nur in `App.OnFrameworkInitializationCompleted` verwendet. Falls mehr Code drauf zugreift → echtes DI durchziehen. |
| Embedded `schema.sql` ohne Versionierung | Solange das Schema nicht in Production läuft, OK. Sobald User-Daten existieren → `migrations/` Folder + Version-Tabelle. |
| CRLF-Warnings beim Commit | `.gitattributes` mit `* text=auto eol=lf` (oder explizit pro Sprache) wäre sauberer. |
---
## Empfohlene Reihenfolge für die nächste Session
1. **Verification Step 4** zusammen durchspielen → falls etwas grundlegend kaputt ist, jetzt finden, nicht später.
2. **CLI-Preflight (3.1)** + **Folder-Picker (2.1)** + **Delete-Confirm (2.2)** — kleine, isolierte Wins.
3. **Auto-Scroll (2.4)** + **Active-Tasks Live-Update (2.6)** — User-Experience im Detail-Pane.
4. **Markdown-Rendering (2.3)** — größer, lohnt sich aber für Lesbarkeit.
5. **Worktree-Cleanup (3.2)** — Robustheit, bevor wir Worktrees ernsthaft nutzen.
6. **CI-Pipeline (5.1)** — automatisches Sicherheitsnetz für alles weitere.
7. **Service-Deployment (4)** — wenn die App lokal stabil läuft.
8. **Settings-Dialog (2.7)** + **Diff-Viewer (2.5)** — Polish.
9. **Tag-Negation (3.4)** — wenn der Bedarf konkret wird.
Punkte 13 sind ein realistischer Block für eine Session.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,514 @@
# Worker CLI Modernization
**Date:** 2026-04-14
**Status:** Approved
**Scope:** ClaudeDo.Worker — CLI invocation, execution tracking, per-task configuration, multi-turn support
## Problem
The Worker currently invokes Claude CLI with hardcoded flags (`-p --output-format stream-json --verbose --dangerously-skip-permissions`). There is no way to configure model, system prompt, or agent per list or task. Execution is single-shot with no retry or follow-up capability. Results are stored as a single markdown blob on the `tasks` row with no structured metadata, token usage, or turn count.
## Goals
1. Per-list configuration (model, system prompt, agent file) with per-task overrides
2. Execution history — each CLI invocation tracked as its own `task_runs` row
3. Multi-turn support — manual continue and auto-retry via `--resume`
4. Structured output alongside markdown via `--json-schema`
5. Agent file management — filesystem-based `.md` agents with UI to browse/create/edit
6. Richer stream parsing — token usage, turn count, session ID, retry events
## Non-Goals (Deferred)
- `--bare` mode (forces API key; user relies on OAuth/keychain auth)
- `--allowedTools` / permission modes (keep `--dangerously-skip-permissions`)
- Schema migration framework (use `IF NOT EXISTS` / `INSERT OR IGNORE` for additive changes)
---
## 1. Schema Changes
### 1.1 New table: `list_config`
One-to-one with `lists`. Stores per-list defaults for CLI invocation.
```sql
CREATE TABLE IF NOT EXISTS list_config (
list_id TEXT PRIMARY KEY REFERENCES lists(id) ON DELETE CASCADE,
model TEXT NULL, -- 'opus-4-6' | 'sonnet-4-6' | 'haiku-4-5'
system_prompt TEXT NULL, -- appended via --append-system-prompt
agent_path TEXT NULL -- path to agent .md file, passed via --agents
);
```
### 1.2 New columns on `tasks`
Per-task overrides. All nullable — NULL means "use list default".
```sql
ALTER TABLE tasks ADD COLUMN model TEXT NULL;
ALTER TABLE tasks ADD COLUMN system_prompt TEXT NULL;
ALTER TABLE tasks ADD COLUMN agent_path TEXT NULL;
```
Since schema uses `IF NOT EXISTS` and is re-applied on startup, these are added via `ALTER TABLE ... ADD COLUMN` wrapped in a try/catch (SQLite raises "duplicate column" if already present — safe to ignore).
### 1.3 New table: `task_runs`
One row per CLI invocation. Supports multi-turn and retry tracking.
```sql
CREATE TABLE IF NOT EXISTS task_runs (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
run_number INTEGER NOT NULL, -- 1, 2, 3... sequential per task
session_id TEXT NULL, -- Claude CLI session ID (for --resume)
is_retry INTEGER NOT NULL DEFAULT 0, -- 0 = normal/continue, 1 = auto-retry
prompt TEXT NOT NULL, -- the prompt sent for this run
result_markdown TEXT NULL, -- free-form result from 'result' field
structured_output TEXT NULL, -- JSON from 'structured_output' field
error_markdown TEXT NULL, -- error output on failure
exit_code INTEGER NULL, -- CLI exit code
turn_count INTEGER NULL, -- number of agent loop turns
tokens_in INTEGER NULL, -- total input tokens
tokens_out INTEGER NULL, -- total output tokens
log_path TEXT NULL, -- NDJSON log file for this run
started_at TIMESTAMP NULL,
finished_at TIMESTAMP NULL
);
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
```
### 1.4 Denormalized fields on `tasks`
Keep existing `result`, `log_path`, `started_at`, `finished_at` on the `tasks` table. After each run completes, update them with the latest run's values. This preserves backward compatibility for UI queries that read `tasks` directly.
### 1.5 Model validation
Valid model values: `opus-4-6`, `sonnet-4-6`, `haiku-4-5`. Validated at the application layer (repository/service), not via SQL CHECK constraint, to allow easy future additions.
---
## 2. Agent File Management
### 2.1 Directory
Agents live in `~/.todo-app/agents/`. The directory is created on Worker startup if absent.
### 2.2 File format
Standard Claude agent markdown with YAML frontmatter:
```markdown
---
name: .NET Developer
description: Senior .NET developer focused on clean architecture
---
You are a senior .NET developer. Follow existing project patterns...
```
### 2.3 AgentFileService
New service in `ClaudeDo.Worker` (not a repository — operates on filesystem, not DB):
| Method | Description |
|--------|-------------|
| `ScanAsync()` | Returns `List<AgentInfo>` — parse frontmatter for name/description from all `*.md` in agents dir |
| `ReadAsync(string path)` | Full file content |
| `WriteAsync(string path, string content)` | Create or overwrite |
| `DeleteAsync(string path)` | Remove file |
### 2.4 AgentInfo DTO
```csharp
public sealed record AgentInfo(string Name, string Description, string Path);
```
### 2.5 Discovery
- Worker scans on startup and exposes agents via a new SignalR method `GetAgents()`.
- UI calls `GetAgents()` to populate dropdowns.
- A `RefreshAgents()` hub method triggers a re-scan (for after UI creates/edits a file).
---
## 3. CLI Invocation Changes
### 3.1 Current invocation
```
claude -p --output-format stream-json --verbose --dangerously-skip-permissions
```
Prompt written to stdin. Single-shot, no config, no structured output.
### 3.2 New invocation
Built dynamically per run by `ClaudeArgsBuilder`:
```
claude -p
--output-format stream-json
--verbose
--dangerously-skip-permissions
--model <resolved-model> # if set
--append-system-prompt <resolved-prompt> # if set
--agents '[{"file":"<resolved-agent-path>"}]' # if set
--json-schema <schema-json> # always
--resume <session-id> # only for multi-turn/retry
```
### 3.3 Config resolution
```
resolved_model = task.model ?? list_config.model ?? null (omit --model)
resolved_prompt = task.system_prompt ?? list_config.system_prompt ?? null (omit --append-system-prompt)
resolved_agent = task.agent_path ?? list_config.agent_path ?? null (omit --agents)
```
### 3.4 Structured output schema
Passed via `--json-schema` on every invocation:
```json
{
"type": "object",
"properties": {
"summary": { "type": "string" },
"files_changed": {
"type": "array",
"items": { "type": "string" }
},
"commit_type": { "type": "string" }
},
"required": ["summary"]
}
```
The CLI returns this in the `structured_output` field of the JSON result event. The markdown result remains in the `result` field.
### 3.5 ClaudeArgsBuilder
New class, single responsibility for argument construction:
```csharp
public sealed class ClaudeArgsBuilder
{
// Returns the full argument string for ProcessStartInfo.Arguments
public string Build(ClaudeRunConfig config);
}
public sealed record ClaudeRunConfig(
string? Model,
string? SystemPrompt,
string? AgentPath,
string? ResumeSessionId
);
```
Testable in isolation — no process spawning, just string building.
---
## 4. Stream Parsing
### 4.1 StreamAnalyzer (replaces MessageParser)
Processes each NDJSON line and accumulates metrics:
| Responsibility | How |
|---|---|
| Extract result markdown | Look for `type: "result"`, read `.result` field |
| Extract structured output | Same event, read `.structured_output` field |
| Extract session ID | Read `.session_id` from the result event |
| Count turns | Count events where `.type == "assistant"` |
| Accumulate tokens | Sum `.usage.input_tokens` and `.usage.output_tokens` from each turn |
| Track retries | Count `system/api_retry` events (informational logging) |
### 4.2 StreamResult
```csharp
public sealed class StreamResult
{
public string? ResultMarkdown { get; set; }
public string? StructuredOutputJson { get; set; }
public string? SessionId { get; set; }
public int TurnCount { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
public int ApiRetryCount { get; set; }
}
```
### 4.3 Extended RunResult
```csharp
public sealed class RunResult
{
public required int ExitCode { get; init; }
public string? ResultMarkdown { get; init; }
public string? ErrorMarkdown { get; init; }
public string? StructuredOutputJson { get; init; }
public string? SessionId { get; init; }
public int TurnCount { get; init; }
public int TokensIn { get; init; }
public int TokensOut { get; init; }
public bool IsSuccess => ExitCode == 0 && ResultMarkdown is not null;
}
```
---
## 5. Multi-Turn & Auto-Retry
### 5.1 Execution flow
```
Task queued
-> Run 1 (run_number=1, is_retry=0)
-> Resolve config (list defaults + task overrides)
-> Build CLI args (no --resume on first run)
-> Spawn claude, stream output, parse via StreamAnalyzer
-> Create task_runs row with all metrics
-> Update denormalized tasks fields
If failure (exit_code != 0):
-> Auto-retry: Run 2 (run_number=2, is_retry=1)
-> Prompt: "The previous attempt failed with:\n\n{error_markdown}\n\nTry again and fix the issues."
-> Uses --resume <session_id> from Run 1
-> Same worktree, same config
-> Create new task_runs row
-> If still fails: mark task Failed, stop
If success (exit_code == 0):
-> Auto-commit in worktree if changes
-> Mark task Done
User triggers "Continue" on finished/failed task:
-> New run (run_number=N+1, is_retry=0)
-> User-provided follow-up prompt
-> Uses --resume <session_id> from last run
-> Task status -> Running -> Done/Failed
```
### 5.2 Rules
- Max 1 auto-retry per task execution (no retry loops)
- Auto-retry reuses the session via `--resume` (full context of prior failure)
- Manual continue works on both Done and Failed tasks
- Each run gets its own log file: `{task_id}_run{N}.ndjson`
- Worktree commit happens only after a successful run
- If Run 1 has no session_id (edge case: CLI crashed before producing one), skip auto-retry
### 5.3 Continue via SignalR
New hub method: `ContinueTask(string taskId, string followUpPrompt)` -> returns `string runId`
Validation:
- Task must exist
- Task must not be currently running
- Previous run must have a session_id
---
## 6. TaskRunner Refactoring
### 6.1 Current flow (TaskRunner.RunAsync)
1. Load list, create worktree/sandbox, mark running
2. Build prompt from title + description
3. Call `_claude.RunAsync(prompt, dir, logPath, taskId, callback, ct)`
4. Handle result: commit on success, mark done/failed
### 6.2 New flow
```csharp
public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct)
{
// 1. Load list + list_config
// 2. Resolve config (merge list_config + task overrides)
// 3. Create worktree/sandbox (unchanged)
// 4. Execute run (see RunOnceAsync below)
// 5. If failed and no prior retry: auto-retry
// 6. Final status update
}
public async Task ContinueAsync(string taskId, string followUpPrompt, string slot, CancellationToken ct)
{
// 1. Load task, last run (for session_id)
// 2. Mark task running
// 3. Execute run with --resume
// 4. Commit if success + worktree
// 5. Final status update
}
private async Task<RunResult> RunOnceAsync(
TaskEntity task, string slot, string runDir, ClaudeRunConfig config,
int runNumber, bool isRetry, string prompt, CancellationToken ct)
{
// 1. Create task_runs row (started_at = now)
// 2. Build log path: {task_id}_run{runNumber}.ndjson
// 3. Build CLI args via ClaudeArgsBuilder
// 4. Spawn ClaudeProcess
// 5. Stream lines to LogWriter + StreamAnalyzer + HubBroadcaster
// 6. Build RunResult from StreamAnalyzer
// 7. Update task_runs row (finished_at, metrics, result)
// 8. Update denormalized tasks fields
// 9. Return RunResult
}
```
### 6.3 ClaudeProcess changes
Simplified — receives pre-built args, no longer constructs its own:
```csharp
public async Task<RunResult> RunAsync(
string arguments, // pre-built by ClaudeArgsBuilder
string prompt, // written to stdin
string workingDirectory,
Func<string, Task> onStdoutLine,
CancellationToken ct)
```
The `StreamAnalyzer` instance is owned by the caller (TaskRunner), not ClaudeProcess. ClaudeProcess just feeds lines via the callback.
---
## 7. Repository Changes
### 7.1 New: TaskRunRepository
| Method | Description |
|--------|-------------|
| `AddAsync(TaskRunEntity)` | Insert new run |
| `UpdateAsync(TaskRunEntity)` | Update after completion |
| `GetByTaskIdAsync(string taskId)` | All runs for a task, ordered by run_number |
| `GetLatestByTaskIdAsync(string taskId)` | Most recent run (for session_id lookup) |
| `GetByIdAsync(string runId)` | Single run |
### 7.2 Extended: ListRepository
| Method | Description |
|--------|-------------|
| `GetConfigAsync(string listId)` | Returns `ListConfigEntity?` |
| `SetConfigAsync(ListConfigEntity)` | Upsert via INSERT OR REPLACE |
### 7.3 New models
```csharp
public sealed class TaskRunEntity
{
public required string Id { get; init; }
public required string TaskId { get; init; }
public required int RunNumber { get; init; }
public string? SessionId { get; set; }
public required bool IsRetry { get; init; }
public required string Prompt { get; init; }
public string? ResultMarkdown { get; set; }
public string? StructuredOutputJson { get; set; }
public string? ErrorMarkdown { get; set; }
public int? ExitCode { get; set; }
public int? TurnCount { get; set; }
public int? TokensIn { get; set; }
public int? TokensOut { get; set; }
public string? LogPath { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? FinishedAt { get; set; }
}
public sealed class ListConfigEntity
{
public required string ListId { get; init; }
public string? Model { get; set; }
public string? SystemPrompt { get; set; }
public string? AgentPath { get; set; }
}
```
---
## 8. SignalR Hub Changes
### 8.1 New server methods
| Method | Description |
|--------|-------------|
| `ContinueTask(string taskId, string followUpPrompt)` | Trigger follow-up run. Returns `string runId`. Throws if running or no session. |
| `GetAgents()` | Returns `List<AgentInfo>` from AgentFileService scan |
| `RefreshAgents()` | Re-scan agents directory |
### 8.2 Updated broadcasts
| Event | Change |
|-------|--------|
| `TaskStarted(slot, taskId, runId, runNumber, startedAt)` | Added `runId`, `runNumber` |
| `TaskFinished(slot, taskId, runId, status, finishedAt)` | Added `runId` |
| `TaskMessage(taskId, runId, ndjsonLine)` | Added `runId` |
| `RunCreated(taskId, runId, runNumber, isRetry)` | New — signals retry/continue started |
### 8.3 Unchanged
`Ping`, `GetActive`, `CancelTask`, `WakeQueue`, `WorktreeUpdated`, `TaskUpdated` — no changes.
---
## 9. File Structure (New/Changed)
```
src/ClaudeDo.Worker/
Runner/
ClaudeArgsBuilder.cs NEW — CLI argument construction
StreamAnalyzer.cs NEW — replaces MessageParser
StreamResult.cs NEW — accumulated stream metrics
RunResult.cs CHANGED — extended with tokens, turns, session_id
ClaudeProcess.cs CHANGED — simplified, takes pre-built args
TaskRunner.cs CHANGED — retry/continue logic, config resolution
MessageParser.cs DELETED — replaced by StreamAnalyzer
Services/
AgentFileService.cs NEW — filesystem agent management
src/ClaudeDo.Data/
Models/
TaskRunEntity.cs NEW
ListConfigEntity.cs NEW
AgentInfo.cs NEW — DTO (name, description, path)
Repositories/
TaskRunRepository.cs NEW
ListRepository.cs CHANGED — GetConfigAsync, SetConfigAsync
schema/
schema.sql CHANGED — list_config table, task_runs table, tasks columns
```
---
## 10. Testing Strategy
### 10.1 Unit tests (new)
| Test class | Covers |
|------------|--------|
| `ClaudeArgsBuilderTests` | Arg construction with all config combos, omitted flags for null values |
| `StreamAnalyzerTests` | Turn counting, token accumulation, result extraction, session_id, retry events, malformed input |
| `AgentFileServiceTests` | Scan, frontmatter parsing, read/write/delete, missing directory handling |
### 10.2 Unit tests (updated)
| Test class | Changes |
|------------|---------|
| `TaskRunnerTests` | New: auto-retry flow, continue flow, config resolution |
| `QueueServiceTests` | New: continue task routing |
### 10.3 Integration tests (new)
| Test class | Covers |
|------------|--------|
| `TaskRunRepositoryTests` | CRUD, ordering, latest-by-task queries |
| `ListRepositoryConfigTests` | GetConfig, SetConfig upsert behavior |
### 10.4 Existing tests (MessageParserTests)
Removed along with `MessageParser`. Equivalent coverage moves to `StreamAnalyzerTests`.

View File

@@ -0,0 +1,29 @@
# ClaudeDo.App
Desktop entry point for the ClaudeDo application. Configures DI, initializes the database, and launches the Avalonia window.
## Responsibility
- `Program.cs` — STA thread, DI container registration (repositories, services, viewmodels), schema init, Avalonia builder
- `App.axaml` / `App.axaml.cs` — Avalonia application lifecycle, main window creation, static `ServiceProvider` accessor
- `ViewLocator.cs` — reflection-based IDataTemplate that maps ViewModels to Views by naming convention
## Dependencies
- Avalonia 12.0.0 (Desktop, Fluent theme, Inter fonts)
- CommunityToolkit.Mvvm 8.4.1
- Microsoft.Extensions.DependencyInjection 8.0.1
- Microsoft.AspNetCore.SignalR.Client 8.0.11
- Microsoft.Data.Sqlite 8.0.11
- Project references: ClaudeDo.Data, ClaudeDo.Ui
## DI Registration Pattern
- **Singletons**: SqliteConnectionFactory, all Repositories, WorkerClient, MainWindowViewModel, TaskListViewModel, TaskDetailViewModel, StatusBarViewModel
- **Transients**: TaskEditorViewModel, ListEditorViewModel (created per dialog)
## Notes
- This project owns the composition root — all wiring happens here
- ViewLocator resolves `FooViewModel` -> `FooView` by replacing "ViewModel" with "View" in the type name
- AvaloniaUI diagnostics are conditionally included (DEBUG only)

View File

@@ -0,0 +1,41 @@
# ClaudeDo.Data
Shared data layer: models, repositories, SQLite infrastructure, and git operations.
## Models
- **TaskEntity** — Id, ListId, Title, Description, Status (Manual|Queued|Running|Done|Failed), ScheduledFor, Result, LogPath, timestamps, CommitType
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
- **TagEntity** — Id (autoincrement), Name (unique)
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
## Repositories
All repositories use raw parameterized SQL via `Microsoft.Data.Sqlite`. Each method opens its own connection — no Unit of Work.
- **TaskRepository** — CRUD, status transitions (`MarkRunningAsync`, `MarkDoneAsync`, `MarkFailedAsync`), `GetNextQueuedAgentTaskAsync` (queue polling), `GetEffectiveTagsAsync` (union of task + list tags), `FlipAllRunningToFailedAsync`
- **ListRepository** — CRUD, tag junction management
- **TagRepository** — `GetOrCreateAsync` (idempotent)
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
## Infrastructure
- **SqliteConnectionFactory** — creates connections, applies WAL mode once, enforces foreign keys via PRAGMA
- **SchemaInitializer** — applies embedded `schema/schema.sql` idempotently (IF NOT EXISTS, INSERT OR IGNORE)
- **Paths** — expands `~` and `%USERPROFILE%`, resolves relative paths. App root: `~/.todo-app`
- **AppSettings** — loads `~/.todo-app/ui.config.json` (DbPath, SignalRUrl)
## 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
## Schema
6 tables: `lists`, `tasks`, `tags`, `list_tags`, `task_tags`, `worktrees`. See `schema/schema.sql`. Seed data: tags "agent" and "manual".
## Conventions
- Enum <-> string mapping via explicit `ToDb()`/`FromDb()` static methods on each enum
- Primary keys are `init`-only strings (GUIDs assigned at creation)
- Nullable fields use `DBNull.Value` checks
- All methods are async with CancellationToken where applicable

49
src/ClaudeDo.Ui/CLAUDE.md Normal file
View File

@@ -0,0 +1,49 @@
# ClaudeDo.Ui
Avalonia UI layer: views, viewmodels, converters, and the SignalR client.
## Pattern
MVVM with CommunityToolkit.Mvvm source generators:
- `[ObservableProperty]` for bindable properties
- `[RelayCommand]` for commands (supports async and CanExecute)
- All ViewModels inherit `ViewModelBase` (extends `ObservableObject`)
## Views
- **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
All views use compiled bindings (`x:DataType`).
## 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
## Services
- **WorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated
## Converters
- **StatusColorConverter** — task status string -> color (Queued=Blue, Running=Orange, Done=Green, Failed=Red, Manual=Gray)
- **ConnectionColorConverter** — connection state -> color (Online=Green, Offline=Red)
## Dialog Pattern
Editor dialogs use `TaskCompletionSource<bool>` — 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
- "Run Now" CanExecute re-evaluates when worker connection state changes

View File

@@ -12,8 +12,10 @@
<TextBox Text="{Binding Name}" PlaceholderText="List name..."/> <TextBox Text="{Binding Name}" PlaceholderText="List name..."/>
<TextBlock Text="Working Directory" FontWeight="SemiBold"/> <TextBlock Text="Working Directory" FontWeight="SemiBold"/>
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/> <DockPanel>
<!-- TODO: folder picker button using IStorageProvider --> <Button DockPanel.Dock="Right" Content="Browse..." Click="OnBrowseFolder" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/>
</DockPanel>
<TextBlock Text="Default Commit Type" FontWeight="SemiBold"/> <TextBlock Text="Default Commit Type" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding CommitTypes}" <ComboBox ItemsSource="{Binding CommitTypes}"

View File

@@ -1,4 +1,9 @@
using System;
using System.IO;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views; namespace ClaudeDo.Ui.Views;
@@ -8,4 +13,28 @@ public partial class ListEditorView : Window
{ {
InitializeComponent(); InitializeComponent();
} }
private async void OnBrowseFolder(object? sender, RoutedEventArgs e)
{
var vm = DataContext as ListEditorViewModel;
var startPath = !string.IsNullOrWhiteSpace(vm?.WorkingDir) && Directory.Exists(vm.WorkingDir)
? vm.WorkingDir
: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var startLocation = await StorageProvider.TryGetFolderFromPathAsync(new Uri(startPath));
var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Working Directory",
SuggestedStartLocation = startLocation,
AllowMultiple = false,
});
if (result.Count > 0)
{
var path = result[0].TryGetLocalPath();
if (path is not null && vm is not null)
vm.WorkingDir = path;
}
}
} }

View File

@@ -20,6 +20,7 @@ builder.Services.AddSingleton<TagRepository>();
builder.Services.AddSingleton<ListRepository>(); builder.Services.AddSingleton<ListRepository>();
builder.Services.AddSingleton<TaskRepository>(); builder.Services.AddSingleton<TaskRepository>();
builder.Services.AddSingleton<WorktreeRepository>(); builder.Services.AddSingleton<WorktreeRepository>();
builder.Services.AddSingleton<TaskRunRepository>();
builder.Services.AddHostedService<StaleTaskRecovery>(); builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
@@ -28,8 +29,14 @@ builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
builder.Services.AddSingleton<HubBroadcaster>(); builder.Services.AddSingleton<HubBroadcaster>();
builder.Services.AddSingleton<GitService>(); builder.Services.AddSingleton<GitService>();
builder.Services.AddSingleton<WorktreeManager>(); builder.Services.AddSingleton<WorktreeManager>();
builder.Services.AddSingleton<ClaudeArgsBuilder>();
builder.Services.AddSingleton<TaskRunner>(); builder.Services.AddSingleton<TaskRunner>();
// Agent file management.
var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents");
Directory.CreateDirectory(agentsDir);
builder.Services.AddSingleton(new AgentFileService(agentsDir));
// QueueService: singleton + hosted service (same instance). // QueueService: singleton + hosted service (same instance).
builder.Services.AddSingleton<QueueService>(); builder.Services.AddSingleton<QueueService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>()); builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>());

View File

@@ -1,33 +0,0 @@
using System.Text.Json;
namespace ClaudeDo.Worker.Runner;
public static class MessageParser
{
public static bool TryExtractResult(string ndjsonLine, out string? result)
{
result = null;
if (string.IsNullOrWhiteSpace(ndjsonLine))
return false;
try
{
using var doc = JsonDocument.Parse(ndjsonLine);
var root = doc.RootElement;
if (root.TryGetProperty("type", out var typeProp) &&
typeProp.GetString() == "result" &&
root.TryGetProperty("result", out var resultProp))
{
result = resultProp.GetString();
return true;
}
}
catch (JsonException)
{
// Malformed JSON — not a result line.
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
# ClaudeDo.Worker.Tests
xUnit integration tests for the Worker and Data layers.
## Framework
- xUnit 2.5.3 with `xunit.runner.visualstudio`
- No mocking library — custom sealed fakes (FakeClaudeProcess, FakeHubContext, FakeHubClients, FakeClientProxy)
- Real SQLite databases per test via `DbFixture`
- Real git repos for worktree tests via `GitRepoFixture`
- coverlet for coverage collection
## Test Infrastructure
- **DbFixture** — creates unique temp SQLite DB, applies schema, cleans up DB + WAL/SHM files on dispose
- **GitRepoFixture** — creates temp git repo with initial commit, configures user/email, handles Windows read-only .git cleanup. Tests skip if git is unavailable.
## Test Areas
| Area | File | What it covers |
|------|------|----------------|
| Repositories | `ListRepositoryTests` | CRUD, tag junctions |
| | `TaskRepositoryTests` | CRUD, status transitions, agent tag filtering, effective tags, stale flip |
| Runner | `WorktreeManagerTests` | Worktree creation, commit detection, error on non-git dir |
| | `CommitMessageBuilderTests` | Slug generation, title/description truncation |
| | `MessageParserTests` | NDJSON parsing, malformed input |
| Services | `QueueServiceTests` | FIFO ordering, override slot contention, cancellation, active tracking |
| | `StaleTaskRecoveryTests` | Flips orphaned running tasks to failed |
## Conventions
- Test classes implement `IDisposable` and create fixtures in constructor
- Helper factory methods for entities: `MakeTask()`, `CreateListAsync()`, `SeedListWithAgentTag()`
- Concurrency tests use `TaskCompletionSource` as gates for deterministic ordering
- Git-dependent tests are conditionally skipped via `Skip = ...` when git is not available
## Running
```bash
dotnet test tests/ClaudeDo.Worker.Tests
```

View File

@@ -1,53 +0,0 @@
using ClaudeDo.Worker.Runner;
namespace ClaudeDo.Worker.Tests.Runner;
public sealed class MessageParserTests
{
[Fact]
public void WellFormed_Result_Line_Extracts_Result()
{
var line = """{"type":"result","result":"Hello **world**"}""";
Assert.True(MessageParser.TryExtractResult(line, out var result));
Assert.Equal("Hello **world**", result);
}
[Fact]
public void Non_Result_Type_Returns_False()
{
var line = """{"type":"assistant","message":"hi"}""";
Assert.False(MessageParser.TryExtractResult(line, out var result));
Assert.Null(result);
}
[Fact]
public void Missing_Type_Property_Returns_False()
{
var line = """{"result":"data"}""";
Assert.False(MessageParser.TryExtractResult(line, out var result));
Assert.Null(result);
}
[Fact]
public void Malformed_Json_Returns_False_No_Throw()
{
var line = "this is not json {{{";
Assert.False(MessageParser.TryExtractResult(line, out var result));
Assert.Null(result);
}
[Fact]
public void Empty_Line_Returns_False()
{
Assert.False(MessageParser.TryExtractResult("", out _));
Assert.False(MessageParser.TryExtractResult(" ", out _));
}
[Fact]
public void Null_Result_Value_Returns_True_With_Null()
{
var line = """{"type":"result","result":null}""";
Assert.True(MessageParser.TryExtractResult(line, out var result));
Assert.Null(result);
}
}