Compare commits
80
Commits
db447f36da
..
v2.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83ea429b8a | ||
|
|
032debc780 | ||
|
|
f822c50102 | ||
|
|
7dcbbf484e | ||
|
|
5872666f31 | ||
|
|
cf9dd1cc94 | ||
|
|
677a4c1853 | ||
|
|
338fc3903d | ||
|
|
57326f1d3b | ||
|
|
8103006e26 | ||
|
|
5115cfc288 | ||
|
|
1e88fefbda | ||
|
|
7661129202 | ||
|
|
7cbd4e66ae | ||
|
|
6e2158d157 | ||
|
|
d4cd202460 | ||
|
|
519ea5a8e0 | ||
|
|
1aaa40b894 | ||
|
|
3e7126b3f2 | ||
|
|
d2ca7fb500 | ||
|
|
10e561f336 | ||
|
|
32c019bd5d | ||
|
|
65db1cdefa | ||
|
|
5ba0b63e03 | ||
|
|
42c70fb28c | ||
|
|
c9ba1e2645 | ||
|
|
394febadeb | ||
|
|
1ee21b560d | ||
|
|
8f8c2a65b2 | ||
|
|
6c5acd09b9 | ||
|
|
a7b88098ab | ||
|
|
ee84a75bd7 | ||
|
|
1cc247a590 | ||
|
|
c871f35513 | ||
|
|
3972ce50a6 | ||
|
|
8ed6e08710 | ||
|
|
194ce58a72 | ||
|
|
b38b0857dd | ||
|
|
334cf1e1d2 | ||
|
|
b126a21c57 | ||
|
|
b1efcdce87 | ||
|
|
e926f4db81 | ||
|
|
20d17c6887 | ||
|
|
bedd2defbb | ||
|
|
8d7ba1e314 | ||
|
|
48de816051 | ||
|
|
cc3013a76e | ||
|
|
eee87f086c | ||
|
|
9804ecefff | ||
|
|
7d29ec0725 | ||
|
|
524836ffe8 | ||
|
|
47e2734357 | ||
|
|
37da35b903 | ||
|
|
6a2a40cd46 | ||
|
|
01cd47ffec | ||
|
|
3a07b319f9 | ||
|
|
c07c1f70a8 | ||
|
|
52c2186999 | ||
|
|
47edab5907 | ||
|
|
87de53e052 | ||
|
|
c82ea2eea1 | ||
|
|
7d636c60dd | ||
|
|
0b6792620c | ||
|
|
fd50a4fb7c | ||
|
|
342a061d94 | ||
|
|
63d8b5c28d | ||
|
|
ab56644ddc | ||
|
|
71050e2634 | ||
|
|
ef7645c6b1 | ||
|
|
92ce7a4a77 | ||
|
|
4dc4fe27e3 | ||
|
|
0ee30bee03 | ||
|
|
b9e0721875 | ||
|
|
92eb654f9b | ||
|
|
724814f770 | ||
|
|
b5464fc533 | ||
|
|
44cdad386c | ||
|
|
58f8b11dbb | ||
|
|
e653677487 | ||
|
|
994e94c2af |
@@ -1,5 +1,37 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.6.0 — 2026-08-04
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- unbreak the update path and cache the download (63d8b5c)
|
||||||
|
- clear task selection when switching lists (ab56644)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.5.0 (71050e2)
|
||||||
|
|
||||||
|
## v2.5.0 — 2026-07-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- move a task to another list via drag & drop (92ce7a4)
|
||||||
|
- sidebar shows queued+running, no auto-monitor seeding (44cdad3)
|
||||||
|
- expose maxParallelExecutions in get_app_settings (e653677)
|
||||||
|
- accent color presets in Settings → General (149e2ad)
|
||||||
|
- allow Done via update_task_status with worktree guard (d569313)
|
||||||
|
- add Let Claude handle it broom button to tasks header (7d6cb2b)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- stop swallowing mutating hub call failures (b5464fc)
|
||||||
|
- pin LogRingBuffer clock in Does_not_throw_when_detached (58f8b11)
|
||||||
|
- propagate HubException from ApproveReviewAsync so blocked merges surface errors (e1807fd)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.4.0 (3fbbd7a)
|
||||||
|
|
||||||
## v2.4.0 — 2026-07-27
|
## v2.4.0 — 2026-07-27
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,106 +1,429 @@
|
|||||||
# ClaudeDo
|
# ClaudeDo
|
||||||
|
|
||||||
A desktop task management app that executes tasks autonomously via [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) in isolated git worktrees.
|
A Windows desktop app that turns your to-do list into a work queue for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
||||||
|
|
||||||
Queue up coding tasks, and ClaudeDo picks them up one by one — each running in its own worktree so your main branch stays clean.
|
Write down what you want done. ClaudeDo picks the task up, runs Claude in an isolated git
|
||||||
|
worktree, and hands you back a diff to review. Your main branch is never touched until you
|
||||||
|
approve.
|
||||||
|
|
||||||
## Architecture
|
It looks and feels like a normal task app — lists, My Day, stars, due dates — except every
|
||||||
|
task can also be *executed*.
|
||||||
|
|
||||||
Two-process system communicating over SignalR:
|
---
|
||||||
|
|
||||||
| Project | Role |
|
## Contents
|
||||||
|---|---|
|
|
||||||
| **ClaudeDo.App** | Avalonia desktop entry point, DI container setup |
|
|
||||||
| **ClaudeDo.Ui** | Views, ViewModels, SignalR client (MVVM) |
|
|
||||||
| **ClaudeDo.Data** | SQLite data layer, repositories, models, GitService |
|
|
||||||
| **ClaudeDo.Worker** | ASP.NET Core hosted service, task queue, Claude CLI runner |
|
|
||||||
|
|
||||||
```
|
- [Is this for you?](#is-this-for-you)
|
||||||
┌────────────────┐ SignalR ┌────────────────┐
|
- [Requirements](#requirements)
|
||||||
│ ClaudeDo.App │◄───────────►│ ClaudeDo.Worker │
|
- [Install](#install)
|
||||||
│ (Avalonia) │ 127.0.0.1 │ (ASP.NET Core) │
|
- [The window](#the-window)
|
||||||
│ │ :47821 │ │
|
- [The core loop](#the-core-loop)
|
||||||
│ ┌────────────┐│ │ ┌────────────┐ │
|
- [Lists and repositories](#lists-and-repositories)
|
||||||
│ │ Ui ││ │ │ TaskQueue │ │
|
- [Writing a task](#writing-a-task)
|
||||||
│ │(ViewModels)││ │ │ Claude CLI │ │
|
- [Ways to run a task](#ways-to-run-a-task)
|
||||||
│ └────────────┘│ │ └────────────┘ │
|
- [Watching work happen](#watching-work-happen)
|
||||||
└───────┬────────┘ └───────┬────────┘
|
- [Reviewing and merging](#reviewing-and-merging)
|
||||||
│ │
|
- [Resolving conflicts](#resolving-conflicts)
|
||||||
└──────────────┬───────────────┘
|
- [Worktrees](#worktrees)
|
||||||
│
|
- [Staying inside your usage limits](#staying-inside-your-usage-limits)
|
||||||
┌───────┴───────┐
|
- [Your daily rhythm](#your-daily-rhythm)
|
||||||
│ ClaudeDo.Data │
|
- [Settings](#settings)
|
||||||
│ (SQLite) │
|
- [Letting Claude drive ClaudeDo](#letting-claude-drive-claudedo)
|
||||||
└───────────────┘
|
- [Updates](#updates)
|
||||||
```
|
- [Where your data lives](#where-your-data-lives)
|
||||||
|
- [Keyboard shortcuts](#keyboard-shortcuts)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [For developers](#for-developers)
|
||||||
|
|
||||||
## Tech Stack
|
---
|
||||||
|
|
||||||
- .NET 8.0
|
## Is this for you?
|
||||||
- Avalonia 12.0.0 (Fluent theme)
|
|
||||||
- SQLite (WAL mode) via Entity Framework Core (EF Core + Migrations)
|
|
||||||
- SignalR for real-time IPC between UI and Worker
|
|
||||||
- CommunityToolkit.Mvvm for source-generated MVVM
|
|
||||||
- Git worktrees for task isolation
|
|
||||||
|
|
||||||
## Prerequisites
|
ClaudeDo is built for one person running many small-to-medium coding jobs across several
|
||||||
|
repositories, mostly unattended.
|
||||||
|
|
||||||
- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
It fits when you:
|
||||||
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
|
||||||
|
- have a backlog of contained changes ("rename this", "add that endpoint", "fix this bug")
|
||||||
|
- want them worked on while you do something else
|
||||||
|
- still want to read every diff before it lands on `main`
|
||||||
|
|
||||||
|
It is *not* a CI system, not a team tool, and not a chat window. There are no pull
|
||||||
|
requests, no reviewers but you, and no cloud component (unless you deliberately turn on the
|
||||||
|
optional online inbox).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Windows 10/11
|
||||||
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), installed and signed in
|
||||||
|
(ClaudeDo runs `claude` as *you* — it never handles your credentials)
|
||||||
- Git
|
- Git
|
||||||
|
- [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) — the installer
|
||||||
|
itself needs it, so install it first if the installer refuses to start
|
||||||
|
|
||||||
## Getting Started
|
## Install
|
||||||
|
|
||||||
|
Run `ClaudeDo.Installer.exe`. It downloads the current release, writes its config, and
|
||||||
|
starts the background worker.
|
||||||
|
|
||||||
|
The wizard asks for:
|
||||||
|
|
||||||
|
| Page | What it decides |
|
||||||
|
|---|---|
|
||||||
|
| **Welcome** | Install folder, and whether Claude may manage your tasks via MCP (see [below](#letting-claude-drive-claudedo)) |
|
||||||
|
| **Data paths** | Where the database, logs, sandboxes and worktrees live |
|
||||||
|
| **Worker** | Port, path to the `claude` binary, and whether the worker starts at logon |
|
||||||
|
|
||||||
|
Re-running the installer later gives you **Update**, **Repair** and **Uninstall** instead
|
||||||
|
of the full wizard. Uninstall asks separately before deleting your tasks and settings.
|
||||||
|
|
||||||
|
ClaudeDo has two parts: the window you see, and a background **worker** that does the
|
||||||
|
actual running. The worker starts at logon and keeps going even when the window is closed —
|
||||||
|
so a long task finishes whether you are watching or not.
|
||||||
|
|
||||||
|
## The window
|
||||||
|
|
||||||
|
Three panes ("islands"), plus a footer:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┬────────────────────────────┬───────────────────────────┐
|
||||||
|
│ LISTS │ TASKS │ DETAILS │
|
||||||
|
│ │ │ │
|
||||||
|
│ My Day │ ▸ Add a task… ENTER │ Fix login redirect │
|
||||||
|
│ Important │ │ ───────────────────── │
|
||||||
|
│ Planned │ OVERDUE │ Steps ▢ ▢ ▣ │
|
||||||
|
│ │ ● Fix login redirect │ Details (markdown) │
|
||||||
|
│ Queue 3 │ │ Files (drop here) │
|
||||||
|
│ Running 1 │ TASKS │ │
|
||||||
|
│ Review 2 │ ● Add CSV export RUNNING │ Worktree · Diff · Merge │
|
||||||
|
│ │ ● Bump deps QUEUED │ │
|
||||||
|
│ MY LISTS │ ● Write changelog │ Output │ Git │ Session │
|
||||||
|
│ LagerApp 7 │ ● Call the tax guy MANUAL │ ┌─────────────────────┐ │
|
||||||
|
│ LogX 2 │ │ │ live Claude output │ │
|
||||||
|
│ ClaudeDo 12 │ │ └─────────────────────┘ │
|
||||||
|
├──────────────┴────────────────────────────┴───────────────────────────┤
|
||||||
|
│ ● Online 5h 41% · 7d 22% worker log line… logs │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Lists** (left) — smart lists, virtual work lists, and your own lists (one per repo)
|
||||||
|
- **Tasks** (middle) — the selected list, grouped into Overdue / Tasks / Completed
|
||||||
|
- **Details** (right) — everything about the selected task, including its live output
|
||||||
|
- **Footer** — worker connection, usage pill, and the latest worker log line (clickable)
|
||||||
|
|
||||||
|
On a narrow window the side panes fold away automatically.
|
||||||
|
|
||||||
|
The grid icon in the title bar opens **Mission Control**, a separate window for watching
|
||||||
|
several running sessions at once.
|
||||||
|
|
||||||
|
## The core loop
|
||||||
|
|
||||||
|
```
|
||||||
|
write it down → queue it → Claude runs it → you review → merge
|
||||||
|
(Idle) (Queued) (Running) (Waiting for (Done)
|
||||||
|
Review)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Capture.** Type into the add box. That is it — a task starts out as a plain reminder.
|
||||||
|
2. **Queue.** Right-click → *Send to queue*. The worker claims queued tasks in order, as
|
||||||
|
many at a time as *Max parallel executions* allows.
|
||||||
|
3. **Run.** ClaudeDo creates a git worktree off your repo (branch `claudedo/<id>`), runs
|
||||||
|
Claude there with your task as the prompt, and streams the output into the detail pane.
|
||||||
|
4. **Review.** On success the task lands in **Waiting for Review** with a committed
|
||||||
|
worktree and a diff. Nothing has touched your working copy.
|
||||||
|
5. **Merge.** *Approve & merge* merges the branch into the target you pick, then marks the
|
||||||
|
task Done. Or reject it with feedback, park it, or cancel it.
|
||||||
|
|
||||||
|
A task that fails is marked **Failed** and keeps its worktree and its log, so you can look
|
||||||
|
at what happened and either continue the same session or reset and retry from scratch.
|
||||||
|
|
||||||
|
## Lists and repositories
|
||||||
|
|
||||||
|
A list becomes *runnable* by giving it a **working directory** — a git repo. Tasks in that
|
||||||
|
list get worktrees off that repo; tasks in a list without a working directory can still be
|
||||||
|
run, but in a scratch sandbox with no git.
|
||||||
|
|
||||||
|
- **Add repos as lists** (`Repositories` menu, or the folder icon) scans folders you point
|
||||||
|
it at and creates one list per git repo it finds. This is the fastest way to get started.
|
||||||
|
- **List settings** (right-click a list) sets the name, working directory, default commit
|
||||||
|
type, the agent defaults for that list, and an optional verify command.
|
||||||
|
- **Manual list** — mark a list as reminders-only. New tasks in it start out manual, so
|
||||||
|
automation never touches them. Good for a "Errands" or "Phone calls" list.
|
||||||
|
- Drag a task onto another list to move it. ClaudeDo refuses to move a running task and
|
||||||
|
warns you when the two lists point at different repos.
|
||||||
|
|
||||||
|
**Smart lists** are always there:
|
||||||
|
|
||||||
|
| List | Contains |
|
||||||
|
|---|---|
|
||||||
|
| **My Day** | What you (or the daily prep) picked for today, plus a pinned Notes row |
|
||||||
|
| **Important** | Starred tasks |
|
||||||
|
| **Planned** | Everything with a date |
|
||||||
|
| **Queue / Running / Review** | Live work — waiting, in flight, and awaiting your review |
|
||||||
|
|
||||||
|
## Writing a task
|
||||||
|
|
||||||
|
The detail pane is where a one-line reminder becomes a brief Claude can act on:
|
||||||
|
|
||||||
|
- **Title + Details** — markdown, with an edit/preview toggle. This is the prompt.
|
||||||
|
- **Steps** — a checklist. Handy for you, and included when you copy the task out.
|
||||||
|
- **Files** — drag files onto the pane (or use *Add file…*) to attach reference material.
|
||||||
|
Attachments are handed to Claude as read-only paths, not pasted into the prompt.
|
||||||
|
- **Star / Schedule / Add to My Day** — the usual task-app affordances.
|
||||||
|
- **Agent settings** — per-task overrides for model, turn budget, extra system prompt,
|
||||||
|
agent file and skills. Anything you do not override shows an *inherited* badge pointing at
|
||||||
|
the list or global default.
|
||||||
|
- **Refine** — hand the task to Claude to sharpen it: it rewrites the title and details into
|
||||||
|
a proper brief. Useful when you captured something in three words.
|
||||||
|
- **Mark as manual** — a MANUAL badge; the queue, the daily prep and every automation skip
|
||||||
|
it. Use it for things only you can do.
|
||||||
|
|
||||||
|
## Ways to run a task
|
||||||
|
|
||||||
|
| Way | What it does | Use it when |
|
||||||
|
|---|---|---|
|
||||||
|
| **Send to queue** | Worker picks it up in order | The normal path |
|
||||||
|
| **Run now** | Skips the queue, starts immediately | You want this one first |
|
||||||
|
| **Continue** | Resumes the last session with a follow-up | "Almost right, now also…" |
|
||||||
|
| **Reset & retry** | Throws the worktree away, re-queues from scratch | The run went sideways |
|
||||||
|
| **Open ConPTY session** | Opens the *real* Claude terminal for this task in Mission Control | You want to drive it yourself, with the task as the starting brief |
|
||||||
|
| **Open planning session** | An interactive session whose job is to break the task into subtasks | The work is too big for one run |
|
||||||
|
| **Let Claude handle it** | Hands a whole list over in one go | You have a pile of small tasks |
|
||||||
|
|
||||||
|
### Planning sessions
|
||||||
|
|
||||||
|
A planning session is a conversation whose output is *structure*, not code. Claude creates
|
||||||
|
child tasks under the parent. You then:
|
||||||
|
|
||||||
|
1. **Finalize plan** — children are chained (each waits for the previous one) but stay Idle.
|
||||||
|
2. **Queue plan** — when you are happy with the list, this queues them all.
|
||||||
|
3. The parent waits in **Waiting for Subtasks** until every child is terminal, then surfaces
|
||||||
|
for review once — you review and merge the *whole unit*, not each child.
|
||||||
|
|
||||||
|
A parked planning session is remembered; the next launch offers to resume, finalize or
|
||||||
|
discard it.
|
||||||
|
|
||||||
|
### "Let Claude handle it"
|
||||||
|
|
||||||
|
Right-click a list → *Let Claude handle it*. You get a checkbox list of that list's open
|
||||||
|
tasks. Confirm, and one Claude session works the selection end to end: it reads them,
|
||||||
|
de-duplicates overlaps, enhances thin descriptions, queues the work, and merges the results.
|
||||||
|
It shows up as its own MANUAL task so you can see what a given run covered, and you review
|
||||||
|
the combined diff when it is finished.
|
||||||
|
|
||||||
|
## Watching work happen
|
||||||
|
|
||||||
|
- **Detail pane, three tabs** — *Output* (live Claude stream), *Git* (worktree, diff,
|
||||||
|
merge), *Session* (result, token/turn counts, subtask outcomes).
|
||||||
|
- **Mission Control** — one tile per running or interactive session, in Focus or Overview
|
||||||
|
mode. ConPTY tiles are the real Claude TUI, embedded: keyboard, colors, `/` commands, all
|
||||||
|
of it. *New session* opens an ad-hoc one that belongs to no task.
|
||||||
|
- **Roadblocks** — when a run gets stuck it reports a roadblock instead of silently failing.
|
||||||
|
The Session tab shows it as its own card with a reply box: answer the question and the
|
||||||
|
same session picks up where it stopped.
|
||||||
|
- **Claude is asking** — an interactive session can put a question in front of you mid-run;
|
||||||
|
answer it inline in Mission Control and the run continues.
|
||||||
|
- **Footer log strip** — the worker's latest event. Failures of your own actions flash here
|
||||||
|
too, instead of vanishing. Click it for the last 30 minutes of worker logs, with a
|
||||||
|
warnings-and-errors filter.
|
||||||
|
|
||||||
|
## Reviewing and merging
|
||||||
|
|
||||||
|
When a task reaches **Waiting for Review** you get four actions:
|
||||||
|
|
||||||
|
| Action | Result |
|
||||||
|
|---|---|
|
||||||
|
| **Approve & merge** | Merges the work into the target branch, then Done |
|
||||||
|
| **Reject** | Asks for feedback and re-runs the same session with it |
|
||||||
|
| **Park** | Back to Idle so you can rewrite the task yourself |
|
||||||
|
| **Cancel** | Done with it; the worktree stays until you clean it up |
|
||||||
|
|
||||||
|
**You have to look before you approve.** When there is a diff to inspect, *Approve & merge*
|
||||||
|
stays disabled until you have opened the diff viewer once. It re-locks after any new run.
|
||||||
|
(The quick-approve on the task row itself is a deliberate bypass for when you already know.)
|
||||||
|
|
||||||
|
The diff viewer shows a file tree on the left and the diff on the right, and can show a
|
||||||
|
dirty worktree, a branch against its base, or a commit range. For a parent with children,
|
||||||
|
*Review combined diff* shows per-subtask diffs plus a combined preview of the whole unit.
|
||||||
|
|
||||||
|
**Verify command** (optional, per list) — a command that must exit 0 after a merge lands
|
||||||
|
before the task is allowed to reach Done. Typically a build or a test run. If it fails, the
|
||||||
|
merge stays (nothing is rewritten behind your back) but the task is held out of Done and the
|
||||||
|
failure output is reported.
|
||||||
|
|
||||||
|
## Resolving conflicts
|
||||||
|
|
||||||
|
If a merge conflicts, ClaudeDo opens a three-pane merge editor in-app:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┬──────────────────┬──────────────────┐
|
||||||
|
│ MAIN │ RESULT │ INCOMING │
|
||||||
|
│ merge target │ (editable) │ task branch │
|
||||||
|
│ │ ▐ │ │
|
||||||
|
│ › │ ← you build │ ‹ │
|
||||||
|
│ │ the result │ │
|
||||||
|
└──────────────────┴──────────────────┴──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Whole files, syntax-highlighted, scrolling in sync
|
||||||
|
- Each conflict starts *empty*. The gutter arrows toggle each side in or out — take main,
|
||||||
|
incoming, both (in the order you click), or neither
|
||||||
|
- Only conflict regions are editable in the middle pane; type your own resolution if
|
||||||
|
neither side is right
|
||||||
|
- `F8` / `Shift+F8` jump between conflicts; the ruler beside the result pane maps every
|
||||||
|
conflict in the file
|
||||||
|
- A counter tells you how many files still need attention. *Resolve & continue* is disabled
|
||||||
|
until they are all done, and *Abort merge* always leaves the target branch as it was
|
||||||
|
|
||||||
|
Binary conflicts cannot be resolved here — ClaudeDo says so and lists the paths.
|
||||||
|
|
||||||
|
## Worktrees
|
||||||
|
|
||||||
|
Every run gets its own worktree, so parallel tasks never fight over your checkout.
|
||||||
|
|
||||||
|
The **Worktrees** overview (per list or global) lists them with state, diff size, age and
|
||||||
|
outcome. From there you can show a diff, jump to the task, merge, batch-merge a selection
|
||||||
|
into one target, mark one *Kept* or *Discarded*, or force-remove a leftover. Worktrees whose
|
||||||
|
directory is gone from disk are flagged as *phantom*.
|
||||||
|
|
||||||
|
Finished worktrees are auto-cleaned after a number of days you choose in Settings.
|
||||||
|
|
||||||
|
## Staying inside your usage limits
|
||||||
|
|
||||||
|
The footer pill shows your Claude usage: `5h 41% · 7d 22%`. Click it for the **Usage
|
||||||
|
Monitor**:
|
||||||
|
|
||||||
|
- Gauges for the 5-hour session window and the 7-day windows, each with a reset countdown
|
||||||
|
- **Models** — token usage per model over a range you pick, split into ClaudeDo's own
|
||||||
|
consumption and everything else
|
||||||
|
- **Tasks** — your biggest consumers, with run counts and totals
|
||||||
|
|
||||||
|
And the part that matters when you are asleep: the **usage limit stop**. Set a percentage
|
||||||
|
per window in Settings, and once usage reaches it the queue simply stops picking up new
|
||||||
|
tasks. Running tasks finish normally, and the queue resumes by itself as the window rolls
|
||||||
|
over. `0` turns a stop off. Manual actions — *Run now*, *Continue*, interactive and planning
|
||||||
|
sessions — are never blocked.
|
||||||
|
|
||||||
|
## Your daily rhythm
|
||||||
|
|
||||||
|
- **My Day** — the day's shortlist. *Clear day* empties it.
|
||||||
|
- **Prime Claude** (Settings → Prime Claude) — schedules for the days and times you pick.
|
||||||
|
At that time ClaudeDo runs a short prep session: Claude looks at your idle tasks, picks an
|
||||||
|
effort-aware subset up to your daily cap, and fills My Day. It also warms your usage
|
||||||
|
window for the day. Only runs while ClaudeDo is open; if you start the app within 30
|
||||||
|
minutes of a scheduled time it fires right away. *Plan day* runs it on demand.
|
||||||
|
- **Notes** — a pinned row in My Day: dated bullet notes with a day navigator, for the
|
||||||
|
thoughts that are not tasks.
|
||||||
|
- **Weekly report** (`Help` menu) — pick a range (defaults to "since your standup weekday")
|
||||||
|
and Claude writes up what actually happened, from your task and session history. Paths you
|
||||||
|
do not want in reports can be excluded in Settings.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Most settings exist at three levels — **global → list → task** — and the more specific one
|
||||||
|
wins. Overridden fields carry an *override* badge with a one-click reset; inherited ones say
|
||||||
|
where they come from.
|
||||||
|
|
||||||
|
**General**
|
||||||
|
- Default instructions applied to every task
|
||||||
|
- Default model, and a **per-model table** of reasoning effort and turn budget — so `haiku`
|
||||||
|
can be cheap and short while `opus` gets room to think
|
||||||
|
- Permission mode for autonomous runs
|
||||||
|
- Max parallel executions
|
||||||
|
- Usage limit stops (see above)
|
||||||
|
- Session skills applied to every task
|
||||||
|
- Report exclusions, standup weekday
|
||||||
|
- Accent color (Moss / Peat / Sea) and language (English / German)
|
||||||
|
|
||||||
|
**Worktrees** — sibling or central placement, auto-cleanup age, and the manual cleanup and
|
||||||
|
force-remove-all buttons.
|
||||||
|
|
||||||
|
**Files** — open the prompt templates (system, planning, retry, daily prep, weekly report)
|
||||||
|
in your editor, and restore the bundled default agent files.
|
||||||
|
|
||||||
|
**Skills** — install a skill from a git URL, update it, remove it. Skills selected globally,
|
||||||
|
per list and per task combine.
|
||||||
|
|
||||||
|
**Prime Claude** — the schedules and the daily task cap.
|
||||||
|
|
||||||
|
**Online Inbox** (optional, off by default) — mirrors your idle backlog to a server so you
|
||||||
|
can capture tasks from your phone. Disabled means zero network traffic. Enabling it needs a
|
||||||
|
URL plus a sign-in; the refresh token is stored encrypted on your machine, never in a config
|
||||||
|
file.
|
||||||
|
|
||||||
|
## Letting Claude drive ClaudeDo
|
||||||
|
|
||||||
|
If you allowed it during install, ClaudeDo registers itself as an MCP server with the Claude
|
||||||
|
CLI. Any Claude session on your machine can then read and manage your tasks: list and create
|
||||||
|
tasks, add subtasks, queue and cancel runs, read logs and diffs, merge, review, manage lists
|
||||||
|
and per-list config.
|
||||||
|
|
||||||
|
Concretely, you can sit in a normal Claude Code session and say "put these five things in
|
||||||
|
the ClaudeDo backlog for the LagerApp list", or "check whether the CSV export task is done".
|
||||||
|
|
||||||
|
The MCP surface is deliberately narrow on the dangerous end: it can start and observe work,
|
||||||
|
but it cannot rewrite your app settings, and a task with an active worktree cannot be forced
|
||||||
|
to Done behind your back.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
ClaudeDo checks for new releases and shows a banner when one is out. *Update now* relaunches
|
||||||
|
the installer, which stops the worker, swaps the binaries and starts it again. Your database,
|
||||||
|
worktrees and settings are preserved. You can also check manually via `Help → Check for
|
||||||
|
updates`.
|
||||||
|
|
||||||
|
## Where your data lives
|
||||||
|
|
||||||
|
Everything is under `%USERPROFILE%\.todo-app`:
|
||||||
|
|
||||||
|
| Path | What |
|
||||||
|
|---|---|
|
||||||
|
| `todo.db` | Your tasks, lists, runs and worktree records (SQLite) |
|
||||||
|
| `ui.config.json` | Window and UI settings |
|
||||||
|
| `worker.config.json` | Worker settings — paths, port, worktree strategy |
|
||||||
|
| `logs/` | Worker and task logs |
|
||||||
|
| `agents/` | Your agent definition files |
|
||||||
|
| `attachments/` | Files you attached to tasks |
|
||||||
|
|
||||||
|
`Help → About` links straight to these folders. Nothing leaves your machine unless you turn
|
||||||
|
on the online inbox.
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `Ctrl+K` | Focus search |
|
||||||
|
| `Ctrl+N` | Focus the add-task box |
|
||||||
|
| `Enter` | Add the task |
|
||||||
|
| `Esc` | Leave the current text field / close a dialog |
|
||||||
|
| `F8` / `Shift+F8` | Next / previous conflict (merge editor) |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Worker not reachable"** — the background worker is not running. The dialog offers to
|
||||||
|
start it; `Worker → Restart worker` does the same later. If it keeps happening, re-run the
|
||||||
|
installer and choose *Repair*.
|
||||||
|
|
||||||
|
**A task failed immediately** — usually the `claude` CLI is not signed in, or the list's
|
||||||
|
working directory is not a git repo. Check the Output tab and the footer log.
|
||||||
|
|
||||||
|
**A task is stuck in Running after a crash** — the worker sweeps orphaned runs to Failed on
|
||||||
|
startup. Restart the worker.
|
||||||
|
|
||||||
|
**Nothing is being picked up** — check the usage pill: if a usage stop is active the queue is
|
||||||
|
paused on purpose. Also check whether the tasks are MANUAL, blocked by a predecessor, or
|
||||||
|
scheduled for later.
|
||||||
|
|
||||||
|
## For developers
|
||||||
|
|
||||||
|
Architecture, build commands and per-project docs live in `CLAUDE.md` (root and one per
|
||||||
|
project under `src/`). Short version: .NET 8, Avalonia UI, SQLite via EF Core, and a
|
||||||
|
SignalR-connected worker process on `127.0.0.1:47821`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
dotnet build src/ClaudeDo.App
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
dotnet build src/ClaudeDo.Worker
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
|
||||||
# Run tests
|
|
||||||
dotnet test tests/ClaudeDo.Worker.Tests
|
|
||||||
|
|
||||||
# Run the app
|
|
||||||
dotnet run --project src/ClaudeDo.App
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. Create a task in the UI and tag it with **"agent"** to mark it for automated execution.
|
|
||||||
2. The Worker picks up queued tasks and runs each one via Claude CLI in an isolated git worktree.
|
|
||||||
3. When done, the worktree can be merged, kept for review, or discarded.
|
|
||||||
|
|
||||||
**Task status flow:** `Manual | Queued → Running → Done | Failed`
|
|
||||||
|
|
||||||
**Worktree state flow:** `Active → Merged | Discarded | Kept`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All data and config lives under `~/.todo-app/`:
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `todo.db` | SQLite database |
|
|
||||||
| `ui.config.json` | UI settings |
|
|
||||||
| `worker.config.json` | Worker settings (worktree strategy, etc.) |
|
|
||||||
| `logs/` | Application logs |
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ClaudeDo.slnx
|
|
||||||
├── src/
|
|
||||||
│ ├── ClaudeDo.App/ # Desktop entry point
|
|
||||||
│ ├── ClaudeDo.Ui/ # Views & ViewModels
|
|
||||||
│ ├── ClaudeDo.Data/ # Data access layer
|
|
||||||
│ └── ClaudeDo.Worker/ # Background task runner
|
|
||||||
├── tests/
|
|
||||||
│ └── ClaudeDo.Worker.Tests/
|
|
||||||
├── schema/
|
|
||||||
│ └── schema.sql # Database schema
|
|
||||||
└── docs/
|
|
||||||
├── plan.md # Architecture & design spec
|
|
||||||
├── open.md # Verification checklist & backlog
|
|
||||||
└── improvement-plan.md # Prioritized improvements
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Handoff — List-handler run on list "Claude do", 2026-08-05
|
||||||
|
|
||||||
|
> **✅ COMPLETED 2026-08-05 (follow-up session).** Everything in §1 and §2 is merged and `main`
|
||||||
|
> verified green. Read §0 below before §1–§8 — §5's diagnosis turned out to be **wrong** and the
|
||||||
|
> rest is now history. Still nothing pushed.
|
||||||
|
|
||||||
|
Repo: `C:\Private\ClaudeDo` · List id: `5f973815-050a-4136-94f0-1506a5d4560a` · Branch: `main` (nothing pushed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. What the follow-up session did (and what §5 got wrong)
|
||||||
|
|
||||||
|
**All merged, `main` green after every step** (Worker 811/811, Data 143/143, Ui 292/292,
|
||||||
|
Localization 16/16, builds 0 new warnings):
|
||||||
|
|
||||||
|
| Merged | Task | Merge commit |
|
||||||
|
|---|---|---|
|
||||||
|
| §1 #1 | `9e307199` revert_merge + merge-SHA persistence | `3e7126b` |
|
||||||
|
| §1 #2 | `0b2fbb48` post-merge verification gate | (conflict-resolved) |
|
||||||
|
| §1 #3 | `8c1c2130` roadblock reply box | `519ea5a` |
|
||||||
|
| §2 #42 | `c1df5b9a` Hub surface for usage | `6e2158d` |
|
||||||
|
| §2 #43 | `f74b44d9` Usage pill | `7661129` |
|
||||||
|
| §2 #45 | `06068810` gate thresholds in settings | `5115cfc` |
|
||||||
|
| §2 #44 | `82488d2a` Usage Monitor modal | `338fc39` |
|
||||||
|
| §2 #46 | `9c8cffe0` docs | `5872666` |
|
||||||
|
| parent | `439a4daf` Usage Monitor unit | approved → Done (empty unit merge; all children already `Merged`) |
|
||||||
|
|
||||||
|
Plus one hand-fix on `main`: `677a4c1` — `UsagePillViewModelTests` never set `Loc.Current`
|
||||||
|
(defaults to a key-echo localizer) and only passed because another test class happened to
|
||||||
|
install a real `Localizer` first; #44's new tests changed the ordering and broke it on `main`.
|
||||||
|
Classic "both branches green, `main` red".
|
||||||
|
|
||||||
|
### §5 is wrong — `"exited with code 1 and no result"` is NOT (only) a CLI crash
|
||||||
|
|
||||||
|
It is a **catch-all** hiding at least three causes. The truth is in the run log's last NDJSON
|
||||||
|
line (`{"type":"result", …}` → `terminal_reason` / `errors` / `result`):
|
||||||
|
|
||||||
|
- **`max_turns`** — what actually killed #40 and #42. `app_settings.model_presets` is `NULL`, so
|
||||||
|
`ModelPresets.Parse` falls back to the shipping defaults, and **sonnet's default is 30 turns**.
|
||||||
|
`AppSettings.DefaultMaxTurns` (100) is only the fallback for an *unrecognized* alias, so it
|
||||||
|
never applies. Every task without an explicit `maxTurns` override got 30 turns.
|
||||||
|
→ Fix used here: `set_task_config(taskId, model="sonnet", maxTurns=200)` before queueing.
|
||||||
|
- **`api_error`** + `"You've hit your session limit · resets 1pm (Europe/Berlin)"` — the account's
|
||||||
|
5-hour limit, which took out #43's first attempt. Nothing to fix; wait for the reset, re-queue.
|
||||||
|
- Genuine process death — the case §5 describes.
|
||||||
|
|
||||||
|
§5's *operational* advice still holds and is what saved #42: **never `reset_failed_task`** on one
|
||||||
|
of these; check the worktree, build/test it, then set the task `Queued` so the agent resumes its
|
||||||
|
own session and commits. #42's worktree held a complete green implementation (773/773).
|
||||||
|
|
||||||
|
Follow-up tasks: `ca6e55c0` (surface the real failure reason) is new; the turn-budget/presets side
|
||||||
|
is already covered by the Idle task `2de2f008` (`b0317ec7` fixed only the unknown-alias half).
|
||||||
|
|
||||||
|
### Still open
|
||||||
|
|
||||||
|
- **Visual passes** (nobody has looked at these in a running app): usage pill in the footer *and*
|
||||||
|
the Mission Control header; Usage Monitor modal (gauges, tables, stale/blocked bands, dark/light);
|
||||||
|
the roadblock reply box; the verify-command field in the List Settings modal. See `docs/open.md`.
|
||||||
|
- **`~/.todo-app/prompts/planning.md` still shadows the compiled default** (§7.1) — unchanged.
|
||||||
|
- **`wait_for_task_change` is merged but not in the running Worker**, so it isn't callable over MCP
|
||||||
|
until the Worker is restarted. §3's sqlite poll was used instead.
|
||||||
|
- One rough edge in the verify gate: if the verify command fails, the worktree has already been
|
||||||
|
removed and its state set `Merged` while the task stays `WaitingForReview` — re-approving is then
|
||||||
|
refused. Recovery is `update_task_status(..., "Done")` once `main` is fixed.
|
||||||
|
|
||||||
|
Predecessor session ran the five-phase list handler over 11 briefed tasks and, along the way,
|
||||||
|
absorbed the 9-child "Usage Monitor" unit. **Phases 0–3 are complete for the brief.** What is
|
||||||
|
left is Phase 4 (review + merge) for three tasks, plus the Usage chain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Do this first — three brief tasks sit in WaitingForReview
|
||||||
|
|
||||||
|
Merge in **this order** (the order was chosen with the user and matters):
|
||||||
|
|
||||||
|
| # | Task | Id | Note |
|
||||||
|
|---|------|----|------|
|
||||||
|
| 1 | Feat: Merge zurücknehmen — Merge-Commit festhalten + `revert_merge` | `9e307199-2eca-4eb7-9057-d2c675cc57ca` | Migration + new tool. Merge **before** #2 |
|
||||||
|
| 2 | Feat: Verifikations-Gate nach dem Merge | `0b2fbb48-d44c-4155-8c21-d3464c0bd5c2` | Depends on #1's merge-SHA persistence; both edit `TaskMergeService.cs` |
|
||||||
|
| 3 | Feat: Antwortfeld auf der Roadblock-Karte | `8c1c213004574c4fad6beb75b84b70d7` | UI + localization (en **and** de) |
|
||||||
|
|
||||||
|
For each one:
|
||||||
|
|
||||||
|
1. `get_task_diff(taskId, stat=true)`, then the full diff if non-trivial. Sanity-check against
|
||||||
|
the task description (they are long and precise — the acceptance criteria are the checklist).
|
||||||
|
2. `review_task(taskId, decision="approve", leaveConflictsInTree=true)`.
|
||||||
|
3. On conflict: open the files under the returned `repoPath`, resolve keeping **both** sides'
|
||||||
|
intent, then `continue_merge(taskId)`. Conflicts are expected and normal here.
|
||||||
|
4. **After every merge, verify `main`** (see §4). This is non-negotiable — see §5.
|
||||||
|
|
||||||
|
Expected conflicts: `TaskMergeService.cs` between #1 and #2; `src/ClaudeDo.Worker/CLAUDE.md`
|
||||||
|
and `src/ClaudeDo.Data/CLAUDE.md` in nearly every merge (doc bullet lists — trivial, keep both
|
||||||
|
sides' entries).
|
||||||
|
|
||||||
|
## 2. Then the Usage Monitor unit
|
||||||
|
|
||||||
|
Parent `439a4daf166f4ab5b0fa415693d2c80d` ("Usage Monitor hinzufügen") is `WaitingForChildren`
|
||||||
|
and has **no worktree of its own**. It has 9 children. Four are merged, one is in flight, four
|
||||||
|
are Idle.
|
||||||
|
|
||||||
|
| Child | Id | State |
|
||||||
|
|---|---|---|
|
||||||
|
| #38 Data: Usage-Gate-Schwellen + Modell-Spalte | `c1c999b6-b800-4b6b-a821-fbc028c15772` | merged `b1efcdc` |
|
||||||
|
| #39 Worker: OAuth-Usage-Client + Poller | `f657e316-ad72-4f45-8036-460841fc8997` | merged `b126a21` |
|
||||||
|
| #40 Worker: UsageGate | `06a7cc32-6ab7-4758-98f4-bee77149b2bf` | merged `1ee21b5` |
|
||||||
|
| #41 Worker: TranscriptUsageReader | `840fdb98-1c0e-4219-8062-c8769233fc14` | merged `334cf1e` |
|
||||||
|
| #42 Worker: Hub-Surface für Usage | `c1df5b9a-b911-4fe8-aab4-5876d9d85793` | **re-queued, in flight — read §5 before touching** |
|
||||||
|
| #43 UI: Usage-Pill | `f74b44d9-7e48-4bfe-9d89-075e194d1fc9` | Idle — queue once #42 is merged |
|
||||||
|
| #45 UI: Gate-Schwellen im Settings-Modal | `06068810-5b5c-4635-80dd-62eeba89fb8c` | Idle — queue once #42 is merged (parallel with #43) |
|
||||||
|
| #44 UI: Usage-Monitor-Modal | `82488d2a-8ff7-41b8-b791-367959a8f827` | Idle — needs #42 **and** #43 merged |
|
||||||
|
| #46 Docs: Usage Monitor | `9c8cffe0-8f7b-401e-a4f0-33b937047082` | Idle — last, after everything is merged |
|
||||||
|
|
||||||
|
**The chain is strictly serial and you must respect it.** Every child forks from `main`, and each
|
||||||
|
one's own description hard-requires the earlier ones. Queueing them all at once is exactly what
|
||||||
|
produced the original roadblock: #40 ran, found its prerequisite types only on unmerged sibling
|
||||||
|
branches, and returned `Done` having written **zero** code. So: merge a child → then queue the
|
||||||
|
next → verify `main` → repeat.
|
||||||
|
|
||||||
|
When the last child is merged the parent surfaces for review by itself; approve it to close the
|
||||||
|
unit (it has no worktree, so it approves straight to Done).
|
||||||
|
|
||||||
|
## 3. Cheap status polling — important
|
||||||
|
|
||||||
|
`list_tasks` and `batch_get_tasks` return full descriptions and **blow the token limit** on this
|
||||||
|
list (`list_tasks` over 52 tasks = ~206,000 chars; that is literally one of the bugs this run
|
||||||
|
fixed). Do not poll with them. Poll the DB read-only instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONIOENCODING=utf-8 python - <<'EOF'
|
||||||
|
import sqlite3
|
||||||
|
c=sqlite3.connect("file:C:/Users/mika.kuns/.todo-app/todo.db?mode=ro",uri=True)
|
||||||
|
for i,s in c.execute("select id,status from tasks"):
|
||||||
|
print(s, i)
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
`list_worktrees` is also compact and safe. **New this run:** `wait_for_task_change(taskIds,
|
||||||
|
timeoutSeconds)` is now merged and is the proper primitive — it returns as soon as any listed
|
||||||
|
task leaves Queued/Running (server-clamped to 170 s). Prefer it over sleeping.
|
||||||
|
|
||||||
|
## 4. Verify main after every merge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
For the UI/localization task (#3 above, and children #43–#45) also:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9 — build individual csproj files, `-c Release` (a running Worker locks Debug).
|
||||||
|
|
||||||
|
Baseline as of this handoff: Worker **753/753**, Data **143/143**, build 0 warnings.
|
||||||
|
|
||||||
|
## 5. The trap that cost this run the most time
|
||||||
|
|
||||||
|
Two children "failed" with `"Claude exited with code 1 and no result"`. **That is a CLI crash,
|
||||||
|
not bad code.** In both cases the worktree held complete work that built with 0 warnings and
|
||||||
|
passed the full suite (#40: 732/732, #42: 766/766) — the run just died before the auto-commit.
|
||||||
|
|
||||||
|
- **Never `reset_failed_task` on such a task** — it discards the worktree and destroys the work.
|
||||||
|
- Instead: `cd` into the worktree, `git status`, build + test it. If green, set the task
|
||||||
|
`Queued`. The worktree is preserved and the agent resumes its own session (`--resume`),
|
||||||
|
finds its work and commits it. That is how #40 was recovered.
|
||||||
|
- #42 is mid-recovery right now via exactly this route. If it failed again, verify its worktree
|
||||||
|
(`C:\Private\.claudedo-worktrees\claude-do\c1df5b9a-b911-4fe8-aab4-5876d9d85793`) before
|
||||||
|
doing anything destructive.
|
||||||
|
|
||||||
|
Second trap: git merges cleanly and the **compiler** still breaks. It happened again this run —
|
||||||
|
`Usage/UsageModels.cs` was an add/add conflict, and `src/ClaudeDo.Worker/CLAUDE.md` merged
|
||||||
|
"cleanly" into a file with the `Usage/` folder documented **twice**. Always read what a clean
|
||||||
|
merge produced, and always run §4.
|
||||||
|
|
||||||
|
## 6. Phase 0–3 decisions already made (do not redo)
|
||||||
|
|
||||||
|
Dedupe: four candidate pairs examined, **nothing cancelled**. Decisions:
|
||||||
|
|
||||||
|
- `05827da5` ↔ `81e37801` — kept both, and `05827da5` was **re-scoped**: its "lean status query"
|
||||||
|
half was removed because `81e37801`'s wait tool covers it. `05827da5` now owns only the
|
||||||
|
brief-description rendering. Both are merged.
|
||||||
|
- `a76d9547` ↔ `99732497` — kept both, `99732497` merged first. Done.
|
||||||
|
- `0b2fbb48` ↔ `9e307199` — kept both, `9e307199` merges first. **This is item #1/#2 in §1.**
|
||||||
|
- `20c78c95` ↔ `a76d9547`(b) — kept both, different actors. Done.
|
||||||
|
|
||||||
|
Phase 2: all 11 tasks carry acceptance criteria, real file+line references and out-of-scope
|
||||||
|
sections. Three that were one-liners were researched and rewritten after asking the user
|
||||||
|
(ConPTY fix approach, maxTurns-only scope, roadblock reply-box design).
|
||||||
|
|
||||||
|
Run config: `maxParallelExecutions` = **3**. No list config exists, so effective max turns was
|
||||||
|
the global **100**; it was raised to **200** per-task on the five heaviest via `set_task_config`.
|
||||||
|
`0b2fbb48`, `9e307199` and `8c1c2130` still carry that override.
|
||||||
|
|
||||||
|
## 7. Open follow-ups worth new tasks
|
||||||
|
|
||||||
|
1. **`~/.todo-app/prompts/planning.md` shadows the planning prompt.** `PromptFiles.EnsureExists`
|
||||||
|
only writes a default when the file is absent, and that file exists (dated Jun 2). The
|
||||||
|
`maxTurns` guidance merged in `65db1cd` therefore **does not reach real planning sessions**
|
||||||
|
until that file is updated by hand. `system.md` and `agent.md` are shadowed too.
|
||||||
|
`merge-helper-system.md`/`merge-helper-initial.md` do **not** exist, so this run's handler
|
||||||
|
prompt changes are live.
|
||||||
|
2. **MCP task DTOs expose no parent/child link.** The 9-child Usage unit had to be reconstructed
|
||||||
|
from `sortOrder` and creation timestamps. `get_task`/`list_tasks` should return
|
||||||
|
`parentTaskId` / `blockedByTaskId`.
|
||||||
|
3. **Visual verification open** on: the ConPTY fix (open a tile on a task whose description
|
||||||
|
contains `->`), and — once merged — the roadblock reply box and the verify-gate field in the
|
||||||
|
list settings modal.
|
||||||
|
4. **`"exited with code 1 and no result"` is too common.** Three runs died that way today, two
|
||||||
|
with finished work. Worth investigating whether the auto-commit step can be made to survive
|
||||||
|
a late CLI crash.
|
||||||
|
5. Nothing has been **pushed**. `main` is 22 commits ahead of `8d7ba1e`.
|
||||||
|
|
||||||
|
## 8. Rules this session operated under
|
||||||
|
|
||||||
|
- Drive merges through the MCP tools. Never raw `git merge` / `reset` / `checkout`.
|
||||||
|
- Hand-resolve only markers the tools left behind, then `continue_merge`. When committing by
|
||||||
|
hand is unavoidable, stage **explicit paths** — never `git add -A`: the main checkout is
|
||||||
|
shared with other sessions.
|
||||||
|
- For a parent/children unit merge, pass the **parent** id to `continue_merge` / `abort_merge`.
|
||||||
|
- Ask the user on anything ambiguous, risky, or destructive.
|
||||||
|
- Never `delete_task` to dedupe — `Cancelled` keeps it visible and resettable.
|
||||||
+44
-1
@@ -50,7 +50,7 @@ Alles gebaut + unit-getestet, aber **nicht visuell verifiziert** — Mika prüft
|
|||||||
Offene Entscheidungen dazu:
|
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.
|
- 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.
|
- `AppSettings.DefaultMaxTurns` ist jetzt tatsächlich verdrahtet (`ModelPresets.For(..., global.DefaultMaxTurns)` in `TaskRunner.ResolveConfigAsync`): reiner Fallback für ein Modell, das auch nach `ModelRegistry.TryNormalizeAlias` auf keine Preset-Zeile trifft — vorher war das Feld tot (hartkodierte 30). Hat weiterhin keinen eigenen Editor mehr (nur die Preset-Tabelle pro Modell); Spalte könnte später entfallen, falls das nie zutrifft.
|
||||||
|
|
||||||
## Beobachtung (offen — Entscheidung Mika)
|
## Beobachtung (offen — Entscheidung Mika)
|
||||||
|
|
||||||
@@ -65,6 +65,49 @@ Offene Entscheidungen dazu:
|
|||||||
LIST column. Plus a real-Claude smoke run of the five phases (dedupe questions, enhancements
|
LIST column. Plus a real-Claude smoke run of the five phases (dedupe questions, enhancements
|
||||||
landing in task descriptions, queued execution, merges).
|
landing in task descriptions, queued execution, merges).
|
||||||
|
|
||||||
|
## Offene Verifikation (2026-08-05)
|
||||||
|
|
||||||
|
- **List handler owns a task (2026-08-05)** — build + unit tests all green, but **not visually
|
||||||
|
verified**: start "Let Claude handle it" on a list, confirm exactly one new task appears in that
|
||||||
|
list (`Idle`, MANUAL badge, title "List handler: <list>"), the Mission Control tile is
|
||||||
|
task-based (Submit for review button present), Submit for review flips it to
|
||||||
|
`WaitingForReview`, and the detail pane's diff/merge card shows the full range of everything the
|
||||||
|
run merged to main (via the new `HandlerBaseCommit`/`HandlerHeadCommit` fallback — no
|
||||||
|
`WorktreeEntity` is created for this task, so the diff comes from `list.WorkingDir` directly).
|
||||||
|
- **Roadblock reply field (2026-08-05)** — build + unit tests all green, but **not visually
|
||||||
|
verified**: on a `Done` (or `WaitingForReview`/`Failed`/`Cancelled`) task with a reported
|
||||||
|
roadblock, the ROADBLOCK card shows a reply textbox + Send button under the roadblock text.
|
||||||
|
Check layout/spacing against the AskUser question card it's modeled on, Enter-to-send, the
|
||||||
|
disabled state + hint text when there's no session ID to resume, and that an override-slot-busy
|
||||||
|
error shows up in the footer log strip (not a modal) with the typed text still in the field.
|
||||||
|
Approve should go straight to `Done` with no merge attempt. Design choice: the review range
|
||||||
|
lives as two new nullable columns directly on `TaskEntity` (not a phantom `WorktreeEntity` row),
|
||||||
|
specifically so `list_worktrees`/the Worktrees overview never see it.
|
||||||
|
|
||||||
|
- **Post-merge verify gate (2026-08-05)** — build + unit tests all green (incl. real-process
|
||||||
|
`VerifyCommandRunner` exit-code/output/timeout tests and `TaskMergeService` success/failure/
|
||||||
|
timeout paths via a fake runner), but **not visually verified**: open a list's Settings modal,
|
||||||
|
confirm the new "VERIFICATION" section renders below Agent with a settable/clearable
|
||||||
|
`VerifyCommand` field; approve a task on a list with a failing command configured and confirm
|
||||||
|
the footer/error surfacing (`ShowErrorAsync`) actually shows the verify failure message instead
|
||||||
|
of silently looking like nothing happened. Also no real-build smoke test (a real `dotnet build`/
|
||||||
|
`dotnet test` invocation as the configured command) — only fast synthetic commands (`exit N`,
|
||||||
|
`ping` for timeout) were exercised.
|
||||||
|
|
||||||
|
## Offene Verifikation (2026-08-05, Usage Monitor)
|
||||||
|
|
||||||
|
- **Visueller Pass Usage-Pill** (Footer **und** Mission-Control-Header): Text/Tooltip,
|
||||||
|
Dot-Zustände normal/warn/stale/blocked, Dark/Light.
|
||||||
|
- **Visueller Pass Usage-Monitor-Modal**: Gauges (dynamisch aus `limits[]`), Modell-/Task-
|
||||||
|
Tabellen, Info-Bänder (stale/gate-blocked), 7d/30d-Presets + Custom-Range, Dark/Light.
|
||||||
|
- **E2E Gate**: das Gate greift real, sobald ein Bucket (`five_hour`/`seven_day`) die
|
||||||
|
konfigurierte Schwelle reißt — Queue-Nachschub pausiert, laufende Runs/`RunNow`/ConPTY/
|
||||||
|
Planning/Prime bleiben unberührt — und die Queue nimmt nach dem Reset selbstständig wieder
|
||||||
|
auf (30s-Backstop, kein persistenter Pause-Zustand).
|
||||||
|
- **Risiko:** der Usage-Endpoint (`GET https://api.anthropic.com/api/oauth/usage`) ist
|
||||||
|
undokumentiert und kann sich ändern; bei Ausfall/Formatänderung ist das Gate wirkungslos
|
||||||
|
(fail-open by design — kein Blocker, aber der Schutz fällt dann aus, ohne dass es auffällt).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bewusst verworfen (nicht erneut vorschlagen)
|
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Usage-Optimierung — Befunde und Messmethodik
|
||||||
|
|
||||||
|
Stand: 2026-08-05. Ausgangsfrage: Wie lassen sich autonome ClaudeDo-Agents
|
||||||
|
token-effizient betreiben? Abrechnung läuft über **Abo-Session-Limits, nicht über
|
||||||
|
die API** — relevant ist roher Token-Verbrauch gegen 5h-/7d-Fenster, nicht Geld.
|
||||||
|
|
||||||
|
Dieses Dokument ist bewusst kompakt gehalten. Was eine Session früh in den Prefix
|
||||||
|
lädt, wird mit jeder weiteren Nachricht multipliziert (siehe Befund 5) — ein
|
||||||
|
30k-Handover-Dokument würde das Problem reproduzieren, das es beschreibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Verbrauchsstruktur
|
||||||
|
|
||||||
|
Gemessen über alle Transcripts unter `C:\Users\mika.kuns\.claude\projects`.
|
||||||
|
|
||||||
|
| Posten | Roh-Token | Anteil |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Cache-Read | 3.118.025.644 | 95,6 % |
|
||||||
|
| Cache-Write | 140.783.227 | 4,3 % |
|
||||||
|
| Output | 22.466.702 | 0,7 % |
|
||||||
|
| Input frisch | 141.496 | 0,0 % |
|
||||||
|
|
||||||
|
**Kontext-Resend = 99,3 % des Rohverbrauchs.** Auch unter API-Preisgewichtung
|
||||||
|
(read 0,1× / write 1,25× / out 5×) bleibt es bei 81,3 %. Die Rangfolge ist gegen
|
||||||
|
jede plausible Gewichtung robust.
|
||||||
|
|
||||||
|
Verstärkung: ~3,6 Mio einzigartiger Inhalt → 1,59 Mrd abgerechnete Prompt-Token
|
||||||
|
in ClaudeDo-Sessions. **Jeder Kontext-Token wird im Schnitt ~425× erneut
|
||||||
|
abgerechnet.**
|
||||||
|
|
||||||
|
## 2. Scope-Split — wer verbraucht
|
||||||
|
|
||||||
|
| Scope | Roh-Token | Anteil |
|
||||||
|
|---|---:|---:|
|
||||||
|
| INTERAKTIV (Mikas eigene Sessions) | 2.690.028.785 | **81,6 %** |
|
||||||
|
| AGENT (ClaudeDo-Runs) | 606.222.055 | **18,4 %** |
|
||||||
|
|
||||||
|
Interaktiv nach Modell: opus-4-8 33,0 % · opus-5 28,5 % · sonnet-5 14,0 % ·
|
||||||
|
fable-5 5,2 %. Agent-Runs fahren überwiegend sonnet-5 — der Default greift, die
|
||||||
|
Modelldisziplin auf Agent-Seite ist in Ordnung.
|
||||||
|
|
||||||
|
**Konsequenz:** ClaudeDo-Optimierung adressiert maximal 18 % des Limits. Der
|
||||||
|
größere Block sind die eigenen Opus-Sessions.
|
||||||
|
|
||||||
|
## 3. Session-Länge ist der Treiber
|
||||||
|
|
||||||
|
Verbrauch ≈ Nachrichten × Ø-Kontext, und der Kontext wächst mit den Nachrichten →
|
||||||
|
**quadratisch**. Eine Session in k Teile schneiden bringt grob 1/k.
|
||||||
|
|
||||||
|
Interaktiv: **Top 20 von 255 Sessions = 55,9 % des Verbrauchs.**
|
||||||
|
|
||||||
|
| Roh-Token | Msgs | Ø Kontext | Modell |
|
||||||
|
|---:|---:|---:|---|
|
||||||
|
| 199.516.097 | 716 | 278.653 | opus-4-8 |
|
||||||
|
| 138.020.247 | 440 | 313.682 | opus-5 |
|
||||||
|
| 123.586.522 | 463 | 266.925 | opus-4-8 |
|
||||||
|
|
||||||
|
## 4. Was den Kontext füllt
|
||||||
|
|
||||||
|
| Tool | Anteil (Agent) | Anteil (interaktiv) |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Read | 59,8 % | 68,2 % |
|
||||||
|
| Bash | 16,3 % | 15,4 % |
|
||||||
|
| Grep | 8,3 % | 5,2 % |
|
||||||
|
|
||||||
|
Read ohne `offset`/`limit`: **57 % (Agent) / 62 % (interaktiv)**.
|
||||||
|
Re-Reads derselben Datei in derselben Session: 18 % der Read-Calls.
|
||||||
|
Subagent-Nutzung: nur 40 Calls bei 1.325 Reads (Agent-Seite) — die
|
||||||
|
Kontext-Firewall ist praktisch ungenutzt. Interaktiv sind Subagent-Sessions
|
||||||
|
dagegen 20 % des Verbrauchs.
|
||||||
|
|
||||||
|
Teuerste Read-Ziele sind God-Files: `TasksIslandViewModel.cs` (1121 Z.),
|
||||||
|
`ExternalMcpService.cs` (1002 Z.), `WorkerHub.cs` (979 Z.).
|
||||||
|
|
||||||
|
## 5. Prefix-Größe × Nachrichtenzahl schlägt alles
|
||||||
|
|
||||||
|
Fallstudie an der Analyse-Session selbst (136 Msgs, 43.492.835 Roh-Token):
|
||||||
|
|
||||||
|
```
|
||||||
|
msg 1 : 39.914
|
||||||
|
msg 3 : 273.822 (+233.908) <-- claude-api-Skill geladen
|
||||||
|
msg 136: 380.327
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Skill in Nachricht 3 wurde danach 136× mitgelesen: **31,8 Mio Token = 73 %
|
||||||
|
der Session**. Zum Vergleich: **alle tool_results zusammen = 21.600 Token
|
||||||
|
(0,05 %)**.
|
||||||
|
|
||||||
|
Discovery ist praktisch gratis, wenn man aggregiert statt Dateien dumpt. Teuer ist
|
||||||
|
ausschließlich, was früh und groß in den Prefix wandert. Derselbe Block in
|
||||||
|
Nachricht 120 geladen hätte ein Zwanzigstel gekostet.
|
||||||
|
|
||||||
|
## 6. Amortisation von Planungssessions
|
||||||
|
|
||||||
|
| Posten | Token |
|
||||||
|
|---|---:|
|
||||||
|
| Analyse-Session gesamt | 43,5 Mio |
|
||||||
|
| davon vermeidbarer Prefix-Ballast | −31,8 Mio |
|
||||||
|
| echte Planungsleistung | ≈ 11,7 Mio |
|
||||||
|
|
||||||
|
Gegenrechnung: **10 von 37 Tasks brauchten einen Retry (27 %)**, 13 von 54 Runs
|
||||||
|
sind Wiederholungen. Ein Agent-Run liegt im Schnitt bei ~11 Mio Roh-Token — ein
|
||||||
|
vermiedener Retry spart also grob eine ganze Planungssession. Dazu ersparte
|
||||||
|
Rediscovery: ein Fund von ~8k Token, den ein Agent in Turn 10 eines 60-Turn-Runs
|
||||||
|
selbst machen müsste, wird danach ~50× mitgelesen = ~400k pro Fund.
|
||||||
|
|
||||||
|
**Fazit: gründliche Planung rechnet sich — aber nur bei schlankem Prefix.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was NICHT hilft (geprüft und verworfen)
|
||||||
|
|
||||||
|
- **`--effort` senken.** Thinking ist 12.536 Token über alle interaktiven
|
||||||
|
Sessions = **0,00 %** des Verbrauchs. Effort zu senken spart nichts und kostet
|
||||||
|
Qualität. Endgültig erledigt.
|
||||||
|
- **Skill-Trigger entschärfen.** Der `claude-api`-Skill kostete in einer Session
|
||||||
|
73 % — feuerte aber nur **2× in 3 Wochen** (2026-07-14, 2026-08-05). Erwarteter
|
||||||
|
Nutzen vernachlässigbar, Risiko (Antworten aus veraltetem Prior) real. Er hat
|
||||||
|
sich in dieser Session sogar bezahlt gemacht: der Hinweis, dass `input_tokens`
|
||||||
|
nur der uncached Rest ist, hat den Accounting-Bug aufgedeckt.
|
||||||
|
- **God-Files splitten.** Eigenes Risiko; Read-Disziplin entschärft das Symptom
|
||||||
|
billiger.
|
||||||
|
|
||||||
|
## Offene Hebel — noch nicht untersucht
|
||||||
|
|
||||||
|
- Interaktive Session-Hygiene: ab wann lohnt `/clear`, messbar an Ø-Kontext?
|
||||||
|
- Modellrouting interaktiv (opus-4-8 + opus-5 = 61,5 % des Accounts).
|
||||||
|
- Subagent-Ökonomie: Firewall-Nutzen gegen Eigenverbrauch (20 % interaktiv).
|
||||||
|
- MCP-Tool-Definitionen im Prefix: Umfang bei ~200 deferred Tools nicht gemessen.
|
||||||
|
- Kalibrierung der tatsächlichen Limit-Gewichtung gegen die Rohtoken-Zahlen
|
||||||
|
(braucht `/usage`-Ausgabe; der OAuth-Endpoint ist für Claude nicht zugänglich,
|
||||||
|
weil `~/.claude/.credentials.json` hart blockiert ist).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Messmethodik (reproduzierbar)
|
||||||
|
|
||||||
|
Datenquelle: `C:\Users\mika.kuns\.claude\projects\**\*.jsonl`, ein Record je
|
||||||
|
Message, Verbrauch in `message.usage`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
raw = (u.get('input_tokens',0) + u.get('output_tokens',0)
|
||||||
|
+ u.get('cache_read_input_tokens',0) + u.get('cache_creation_input_tokens',0))
|
||||||
|
```
|
||||||
|
|
||||||
|
Kontextgröße einer Message = `input_tokens + cache_read + cache_write` (ohne
|
||||||
|
Output). Der Verlauf über die Session zeigt Prefix-Sprünge.
|
||||||
|
|
||||||
|
### Fallstricke
|
||||||
|
|
||||||
|
1. **Scope-Filter.** NICHT nach Pfadname `claudedo` filtern — das zieht
|
||||||
|
interaktive Sessions im ClaudeDo-Repo mit rein und verfälscht den Split massiv
|
||||||
|
(53 % statt korrekt 18,4 %). Agent-Runs erkennt man an `claudedo-worktrees`
|
||||||
|
oder `sandbox` im Projektordner, so wie es `TranscriptUsageReader` macht.
|
||||||
|
2. **`<synthetic>`-Messages** überspringen — keine echten API-Calls.
|
||||||
|
3. **`task_runs.tokens_in` ist unbrauchbar** — liest nur `input_tokens`, also den
|
||||||
|
uncached Rest. Lag bei einer 79-Mio-Token-Session bei 200 (Faktor ~400.000).
|
||||||
|
`tokens_out` zusätzlich 3,8× zu niedrig. Siehe Task `38394081`.
|
||||||
|
4. **In Bash absolute Pfade verwenden** — `$HOME`/`~` zeigt auf dieser Maschine
|
||||||
|
auf das P:-Laufwerk, nicht auf `C:\Users\mika.kuns`.
|
||||||
|
5. **DB nie direkt lesen** während die App läuft — Kopie ziehen (`todo.db` +
|
||||||
|
`todo.db-wal`).
|
||||||
|
6. **Grundrate prüfen, bevor ein Hebel empfohlen wird.** Ein teurer Einzelfall ist
|
||||||
|
kein Muster (siehe Skill-Trigger oben).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abgeleitete Tasks (Liste „Claude do")
|
||||||
|
|
||||||
|
Empfohlene Reihenfolge:
|
||||||
|
|
||||||
|
| # | ID | Titel |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `1b599d67` | Prompt-Dateien frieren Default ein — `SuggestImprovement`/`AskUser` tot |
|
||||||
|
| 2 | `38394081` | Limit-Verbrauch pro Run sichtbar machen (Cache-Token in `task_runs`) |
|
||||||
|
| 3 | `0d0aa8b0` | Read-Disziplin + Explorer-Subagent im System-Prompt |
|
||||||
|
| 4 | `2de2f008` | `max_turns` deckeln (Ceiling, Defaults, UI-Warnung) |
|
||||||
|
| 5 | `87105f5e` | UsageGate: Parallelität stufenweise drosseln |
|
||||||
|
|
||||||
|
(1) zuerst, weil es ein echter Funktionsbug ist und Voraussetzung dafür, dass (3)
|
||||||
|
den Nutzer überhaupt erreicht. (2) als Nächstes, weil ohne korrektes Accounting
|
||||||
|
nichts messbar ist.
|
||||||
|
|
||||||
|
### Ist-Zustand zum Zeitpunkt der Analyse
|
||||||
|
|
||||||
|
- `app_settings`: `default_model` = sonnet, `default_max_turns` = 100,
|
||||||
|
`max_parallel_executions` = 3, `model_presets` = **null** (fällt auf Defaults
|
||||||
|
zurück: haiku 20 / sonnet 30 / opus 40 / fable 25 — greifen faktisch nie).
|
||||||
|
- 15 Tasks überschreiben `max_turns` nach oben: 10× auf 100, 5× auf 200.
|
||||||
|
- UsageGate existiert bereits: 5h @ 80 %, 7d @ 90 %
|
||||||
|
(Migration `20260805074906_AddUsageGateAndRunModel`, dieselbe Migration hat auch
|
||||||
|
die `model`-Spalte auf `task_runs` gebracht).
|
||||||
|
- `~/.todo-app/prompts/system.md` stammt vom 2026-06-04 und weicht vom
|
||||||
|
`SystemDefault` in `PromptFiles.cs` ab; `agent.md` und `planning.md` dort sind
|
||||||
|
verwaiste Reste eines alten Namensschemas.
|
||||||
@@ -138,6 +138,8 @@ sealed class Program
|
|||||||
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
||||||
sc.AddTransient<WeeklyReportModalViewModel>();
|
sc.AddTransient<WeeklyReportModalViewModel>();
|
||||||
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
||||||
|
sc.AddTransient<UsageMonitorModalViewModel>();
|
||||||
|
sc.AddTransient<Func<UsageMonitorModalViewModel>>(sp => () => sp.GetRequiredService<UsageMonitorModalViewModel>());
|
||||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||||
sp.GetRequiredService<IWorkerClient>(), taskId));
|
sp.GetRequiredService<IWorkerClient>(), taskId));
|
||||||
@@ -159,10 +161,13 @@ sealed class Program
|
|||||||
sp,
|
sp,
|
||||||
sp.GetRequiredService<INotesApi>(),
|
sp.GetRequiredService<INotesApi>(),
|
||||||
sp.GetRequiredService<IMergeCoordinator>()));
|
sp.GetRequiredService<IMergeCoordinator>()));
|
||||||
|
sc.AddSingleton<UsagePillViewModel>(sp =>
|
||||||
|
new UsagePillViewModel(sp.GetRequiredService<IWorkerClient>()));
|
||||||
sc.AddSingleton<MissionControlViewModel>(sp =>
|
sc.AddSingleton<MissionControlViewModel>(sp =>
|
||||||
new MissionControlViewModel(
|
new MissionControlViewModel(
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||||
sp.GetRequiredService<IWorkerClient>()));
|
sp.GetRequiredService<IWorkerClient>(),
|
||||||
|
sp.GetRequiredService<UsagePillViewModel>()));
|
||||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||||
{
|
{
|
||||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
|||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, 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.
|
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, IsManual (reminder only the user can do — automation skips it), Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit (nullable; review range for a worktree-less "list handler" host task — Mission Control's "Let Claude handle it" — which commits straight into the list's working dir instead of a per-task worktree: `HandlerBaseCommit` is stamped to the list repo's HEAD when the host task is created, `HandlerHeadCommit` when it's submitted for review; the Worker's `SubmitTaskForReview`/`GetTaskDiff` and the Ui's `DetailsIslandViewModel`/`MergeSectionViewModel` fall back to this pair whenever `Worktree` is null). Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`)
|
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`)
|
||||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns (all nullable)
|
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand (all nullable). `VerifyCommand` is an optional post-merge gate (see `ClaudeDo.Worker/CLAUDE.md` → TaskMergeService): null/blank = today's behavior, no gate.
|
||||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, MergeCommit (nullable; SHA of the merge commit this worktree's branch produced on the target branch, stamped by `TaskMergeService` the moment a merge/continue-merge succeeds — the only thing that makes `revert_merge` possible without heuristically searching `git log`; null for any worktree merged before this field existed), State (Active|Merged|Discarded|Kept)
|
||||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path, nullable `Model` — the model the run actually executed with, column `model`)
|
||||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||||
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
||||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) and `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`)
|
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100)
|
||||||
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and never throw — a malformed settings row must not stop a run. `For(presets, model)` always returns a usable row. Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and never throw — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns = 30)` always returns a usable row: `model` is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`) still hits its alias's preset row instead of missing every lookup and falling through; only a model that normalizes to nothing recognized falls back to a synthesized row (`EffortRegistry.DefaultLevel` + `fallbackMaxTurns` — callers pass `AppSettings.DefaultMaxTurns` here so that setting has a real effect instead of a hardcoded number). Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||||
|
- **ModelRegistry.TryNormalizeAlias** — non-throwing counterpart to `NormalizeAlias` for the run path: exact alias match, then substring match against a full model id, else `null`. Never throws, unlike `NormalizeAlias` (which stays the strict, throwing validator for `add_task`/planning model input).
|
||||||
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
|
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
|
||||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
|||||||
|
|
||||||
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
||||||
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
||||||
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
|
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`, `SetMergedAsync` (atomically sets State=Merged and stamps MergeCommit in one update — the only writer of MergeCommit)
|
||||||
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||||
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
||||||
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
||||||
@@ -40,11 +41,11 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
|||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
|
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), revert (`RevertMergeCommitAsync` — `git revert --no-edit -m 1 <sha>`, reverts a merge commit against its first parent; `RevertAbortAsync`; `IsMidRevertAsync` — `REVERT_HEAD` presence, mirrors `IsMidMergeAsync`'s `MERGE_HEAD`), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo. Revert never resets/rewrites — it always produces a new commit, because the working directory it operates on is shared with other concurrent sessions.
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. 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).
|
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
||||||
builder.Property(s => s.ModelPresets).HasColumnName("model_presets");
|
builder.Property(s => s.ModelPresets).HasColumnName("model_presets");
|
||||||
|
|
||||||
|
builder.Property(s => s.UsageGateFiveHourPct)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct").IsRequired().HasDefaultValue(80);
|
||||||
|
builder.Property(s => s.UsageGateSevenDayPct)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct").IsRequired().HasDefaultValue(90);
|
||||||
|
|
||||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
|||||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||||
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
||||||
|
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
||||||
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class TaskRunEntityConfiguration : IEntityTypeConfiguration<TaskRunEntity
|
|||||||
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
||||||
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
||||||
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
||||||
|
builder.Property(r => r.Model).HasColumnName("model");
|
||||||
|
|
||||||
builder.HasOne(r => r.Task)
|
builder.HasOne(r => r.Task)
|
||||||
.WithMany(t => t.Runs)
|
.WithMany(t => t.Runs)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEnti
|
|||||||
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
||||||
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
|
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
|
||||||
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
||||||
|
builder.Property(w => w.MergeCommit).HasColumnName("merge_commit");
|
||||||
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
||||||
.HasDefaultValue(WorktreeState.Active)
|
.HasDefaultValue(WorktreeState.Active)
|
||||||
.HasConversion(StateConverter);
|
.HasConversion(StateConverter);
|
||||||
|
|||||||
@@ -280,6 +280,36 @@ public sealed class GitService
|
|||||||
throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
|
throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsMidRevertAsync(string repoDir, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["rev-parse", "--git-dir"], ct);
|
||||||
|
if (exitCode != 0) return false;
|
||||||
|
var gitDir = stdout.Trim();
|
||||||
|
if (!Path.IsPathRooted(gitDir))
|
||||||
|
gitDir = Path.Combine(repoDir, gitDir);
|
||||||
|
return File.Exists(Path.Combine(gitDir, "REVERT_HEAD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverts a single commit with `-m 1` (diff against its first parent) — the form needed to
|
||||||
|
/// revert a merge commit. On success this creates a new commit with the inverse changes;
|
||||||
|
/// the original commit and all history stay intact (no rewrite, no reset).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(int ExitCode, string Stderr)> RevertMergeCommitAsync(
|
||||||
|
string repoDir, string mergeCommitSha, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||||
|
["-c", "merge.conflictStyle=diff3", "revert", "--no-edit", "-m", "1", mergeCommitSha], ct);
|
||||||
|
return (exitCode, stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertAbortAsync(string repoDir, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["revert", "--abort"], ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new InvalidOperationException($"git revert --abort failed (exit {exitCode}): {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
|
public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
||||||
|
|||||||
+810
@@ -0,0 +1,810 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260805065217_AddHandlerCommitRange")]
|
||||||
|
partial class AddHandlerCommitRange
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddHandlerCommitRange : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "handler_base_commit",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "handler_head_commit",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "handler_base_commit",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "handler_head_commit",
|
||||||
|
table: "tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+828
@@ -0,0 +1,828 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260805074906_AddUsageGateAndRunModel")]
|
||||||
|
partial class AddUsageGateAndRunModel
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUsageGateAndRunModel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "model",
|
||||||
|
table: "task_runs",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "usage_gate_five_hour_pct",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 80);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "usage_gate_seven_day_pct",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 90);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "usage_gate_five_hour_pct", "usage_gate_seven_day_pct" },
|
||||||
|
values: new object[] { 80, 90 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "model",
|
||||||
|
table: "task_runs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "usage_gate_five_hour_pct",
|
||||||
|
table: "app_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "usage_gate_seven_day_pct",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,832 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260805090016_AddVerifyCommand")]
|
||||||
|
partial class AddVerifyCommand
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("VerifyCommand")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("verify_command");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddVerifyCommand : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "verify_command",
|
||||||
|
table: "list_config",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "verify_command",
|
||||||
|
table: "list_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+832
@@ -0,0 +1,832 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260805091910_AddWorktreeMergeCommit")]
|
||||||
|
partial class AddWorktreeMergeCommit
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("MergeCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("merge_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorktreeMergeCommit : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "merge_commit",
|
||||||
|
table: "worktrees",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "merge_commit",
|
||||||
|
table: "worktrees");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,18 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasDefaultValue(3)
|
.HasDefaultValue(3)
|
||||||
.HasColumnName("standup_weekday");
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
b.Property<int>("WorktreeAutoCleanupDays")
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -122,6 +134,8 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
DefaultPermissionMode = "auto",
|
DefaultPermissionMode = "auto",
|
||||||
MaxParallelExecutions = 1,
|
MaxParallelExecutions = 1,
|
||||||
StandupWeekday = 3,
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
WorktreeAutoCleanupDays = 7,
|
WorktreeAutoCleanupDays = 7,
|
||||||
WorktreeAutoCleanupEnabled = false,
|
WorktreeAutoCleanupEnabled = false,
|
||||||
WorktreeStrategy = "sibling"
|
WorktreeStrategy = "sibling"
|
||||||
@@ -184,6 +198,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("system_prompt");
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("VerifyCommand")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("verify_command");
|
||||||
|
|
||||||
b.HasKey("ListId");
|
b.HasKey("ListId");
|
||||||
|
|
||||||
b.ToTable("list_config", (string)null);
|
b.ToTable("list_config", (string)null);
|
||||||
@@ -416,6 +434,14 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("finished_at");
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
b.Property<bool>("IsManual")
|
b.Property<bool>("IsManual")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -572,6 +598,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("log_path");
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
b.Property<string>("Prompt")
|
b.Property<string>("Prompt")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
@@ -681,6 +711,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("head_commit");
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("MergeCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("merge_commit");
|
||||||
|
|
||||||
b.Property<string>("Path")
|
b.Property<string>("Path")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
|
|||||||
@@ -34,4 +34,8 @@ public sealed class AppSettingsEntity
|
|||||||
// JSON array of ModelPreset rows (model → effort + max turns). Supplies the global effort and
|
// 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.
|
// turn defaults per model; list-/task-level max-turns overrides still win. Null = ship defaults.
|
||||||
public string? ModelPresets { get; set; }
|
public string? ModelPresets { get; set; }
|
||||||
|
|
||||||
|
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||||
|
public int UsageGateFiveHourPct { get; set; } = 80;
|
||||||
|
public int UsageGateSevenDayPct { get; set; } = 90;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public sealed class ListConfigEntity
|
|||||||
public string? AgentPath { get; set; }
|
public string? AgentPath { get; set; }
|
||||||
public int? MaxTurns { get; set; }
|
public int? MaxTurns { get; set; }
|
||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
|
public string? VerifyCommand { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public ListEntity List { get; set; } = null!;
|
public ListEntity List { get; set; } = null!;
|
||||||
|
|||||||
@@ -46,20 +46,26 @@ public static class ModelPresets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
|
/// <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>
|
/// callers don't have to special-case an unconfigured or unknown model. <paramref name="model"/>
|
||||||
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model)
|
/// is resolved through <see cref="ModelRegistry.TryNormalizeAlias"/> first, so a full CLI model
|
||||||
|
/// id (e.g. "claude-sonnet-4-6") still hits its alias's preset row instead of falling through.
|
||||||
|
/// If <paramref name="model"/> normalizes to nothing recognized, <paramref name="fallbackMaxTurns"/>
|
||||||
|
/// (typically <c>AppSettings.DefaultMaxTurns</c>) is used instead of a hardcoded number, so a
|
||||||
|
/// truly unknown model is no harsher than the configured global default.</summary>
|
||||||
|
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model, int fallbackMaxTurns = 30)
|
||||||
{
|
{
|
||||||
var alias = (model ?? ModelRegistry.DefaultAlias).Trim();
|
var alias = ModelRegistry.TryNormalizeAlias(model) ?? (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||||
foreach (var p in presets)
|
foreach (var p in presets)
|
||||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
return p;
|
return p;
|
||||||
foreach (var p in Defaults)
|
foreach (var p in Defaults)
|
||||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||||
return p;
|
return p;
|
||||||
return new ModelPreset(alias, EffortRegistry.DefaultLevel, 30);
|
return new ModelPreset(alias, EffortRegistry.DefaultLevel, fallbackMaxTurns);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ModelPreset For(string? json, string? model) => For(Parse(json), model);
|
public static ModelPreset For(string? json, string? model, int fallbackMaxTurns = 30)
|
||||||
|
=> For(Parse(json), model, fallbackMaxTurns);
|
||||||
|
|
||||||
// One row per known alias, canonical order, clamped values — so an edited or partial list
|
// 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.
|
// still round-trips into something every consumer can rely on.
|
||||||
|
|||||||
@@ -26,4 +26,22 @@ public static class ModelRegistry
|
|||||||
return alias;
|
return alias;
|
||||||
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort alias lookup for run-path model resolution, where a full CLI model id
|
||||||
|
/// (e.g. "claude-sonnet-4-6") is just as likely as a bare alias and a run must never be
|
||||||
|
/// blocked by an unrecognized one. Null/blank/unrecognized → null; never throws.
|
||||||
|
/// </summary>
|
||||||
|
public static string? TryNormalizeAlias(string? model)
|
||||||
|
{
|
||||||
|
var m = model?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(m)) return null;
|
||||||
|
foreach (var alias in Aliases)
|
||||||
|
if (string.Equals(alias, m, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return alias;
|
||||||
|
foreach (var alias in Aliases)
|
||||||
|
if (m.Contains(alias, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return alias;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ public sealed class TaskEntity
|
|||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
|
|
||||||
|
// Review range for a worktree-less task hosting an interactive "list handler" run
|
||||||
|
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
||||||
|
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
||||||
|
// capture the repo's HEAD at session start / submit-for-review instead, so the normal
|
||||||
|
// diff/get_task_diff paths can show `HandlerBaseCommit..HandlerHeadCommit` over the
|
||||||
|
// list's working dir exactly like a merged task's commit-range diff.
|
||||||
|
public string? HandlerBaseCommit { get; set; }
|
||||||
|
public string? HandlerHeadCommit { get; set; }
|
||||||
|
|
||||||
public string? ParentTaskId { get; set; }
|
public string? ParentTaskId { get; set; }
|
||||||
public string? PlanningSessionId { get; set; }
|
public string? PlanningSessionId { get; set; }
|
||||||
public string? PlanningSessionToken { get; set; }
|
public string? PlanningSessionToken { get; set; }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public sealed class TaskRunEntity
|
|||||||
public string? LogPath { get; set; }
|
public string? LogPath { get; set; }
|
||||||
public DateTime? StartedAt { get; set; }
|
public DateTime? StartedAt { get; set; }
|
||||||
public DateTime? FinishedAt { get; set; }
|
public DateTime? FinishedAt { get; set; }
|
||||||
|
public string? Model { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public TaskEntity Task { get; set; } = null!;
|
public TaskEntity Task { get; set; } = null!;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class WorktreeEntity
|
|||||||
public required string BaseCommit { get; set; }
|
public required string BaseCommit { get; set; }
|
||||||
public string? HeadCommit { get; set; }
|
public string? HeadCommit { get; set; }
|
||||||
public string? DiffStat { get; set; }
|
public string? DiffStat { get; set; }
|
||||||
|
public string? MergeCommit { get; set; }
|
||||||
public WorktreeState State { get; set; } = WorktreeState.Active;
|
public WorktreeState State { get; set; } = WorktreeState.Active;
|
||||||
public required DateTime CreatedAt { get; init; }
|
public required DateTime CreatedAt { get; init; }
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,12 @@ public static class PromptFiles
|
|||||||
- sonnet — normal coding work; the sensible default when unsure.
|
- sonnet — normal coding work; the sensible default when unsure.
|
||||||
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
||||||
Do not default everything to opus — most subtasks are haiku or sonnet.
|
Do not default everything to opus — most subtasks are haiku or sonnet.
|
||||||
|
|
||||||
|
Only for a subtask you can tell will need noticeably more turns than the
|
||||||
|
default budget (a large or sprawling piece of work), also pass CreateChildTask's
|
||||||
|
`maxTurns` argument with a generous turn count, so the run doesn't die mid-work
|
||||||
|
at the turn limit. Leave `maxTurns` null for everything else — it inherits the
|
||||||
|
list/global default.
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private const string PlanningInitialDefault = """
|
private const string PlanningInitialDefault = """
|
||||||
@@ -229,23 +235,25 @@ public static class PromptFiles
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
private const string MergeHelperDefault = """
|
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.
|
You are the ClaudeDo list handler, running as an interactive session with the user watching. Work autonomously and decide things yourself by default. Ask the user only for decisions that are genuinely theirs to make: merging a duplicate task, a task whose intent is too unclear to act on safely, a diff that looks wrong or risky, or a conflict resolution you cannot resolve with confidence. Everything else, decide and keep moving.
|
||||||
|
|
||||||
Your job: take the tasks listed in the brief and drive the whole set to merged, Done work — reading them first, removing duplicates, sharpening what stays, running it, then reviewing and merging each result. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
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.
|
Work the five phases in order. Do not start a phase before the previous one is finished.
|
||||||
|
|
||||||
## Phase 0 — Read everything
|
## 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.
|
The brief is the primary source: it already lists every task's title, id, status and full description. Read it in full before acting. Only call batch_get_tasks if you need something the brief does not carry for a specific task, e.g. parent/child links. Do not act on any single task before you have read them all — Phase 1 needs the whole set in view.
|
||||||
|
|
||||||
## Phase 1 — Dedupe
|
## 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.
|
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:
|
If no pair looks like a duplicate, say so in one sentence and move straight to Phase 2 — do not ask the user to confirm the absence of duplicates.
|
||||||
|
|
||||||
|
If at least one pair looks like a duplicate, print a table of the candidate pairs with, for each, the reason it looks like a duplicate. Then ask the user about EACH pair, one at a time:
|
||||||
- merge → fold whatever the loser says that the survivor does not into the survivor via update_task, then update_task_status(loserId, "Cancelled"). Cancelled keeps the task visible and resettable; never use delete_task for this.
|
- 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.
|
- keep both → note why and move on.
|
||||||
|
|
||||||
Cancel nothing without an explicit answer. If there are no duplicates, say so and go on.
|
Cancel nothing without an explicit answer.
|
||||||
|
|
||||||
## Phase 2 — Enhance for execution
|
## 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:
|
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:
|
||||||
@@ -257,28 +265,36 @@ public static class PromptFiles
|
|||||||
|
|
||||||
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.
|
Rules: do not change what the user asked for, and do not invent requirements. You are making the existing intent precise, not adding to it. If a task is too vague to sharpen without guessing, ASK instead of guessing. Report a short before/after per task.
|
||||||
|
|
||||||
|
If a task visibly bundles several independent features, or has a blocker that is not resolved by anything in its own description, do not force it into one description. Propose splitting it to the user; if they agree, create the pieces with add_task/add_subtask and only move the pieces the user confirmed into "surviving tasks" for the phases below. Split only what the task already asks for — the "do not invent requirements" rule still applies.
|
||||||
|
|
||||||
## Phase 3 — Run
|
## 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".
|
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.
|
Read get_app_settings and tell the user how many parallel execution slots are configured (maxParallelExecutions). If it is 1, say plainly that the tasks will execute one after another and that the value is changeable in ClaudeDo's settings.
|
||||||
|
|
||||||
|
For each surviving task, resolve the effective max-turns it will run with — task.MaxTurns, else the list's get_list_config MaxTurns, else the model's preset/global default — and report it. If a task looks substantial (several files, or one you just split off above) but its effective turns look low, say so and ask before queuing it; set_task_config/get_task_config let you raise it per task.
|
||||||
|
|
||||||
Then, for each surviving task:
|
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.
|
- 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.
|
- Queued → leave it; it is already waiting for a slot.
|
||||||
- Running or WaitingForChildren → leave it; only poll.
|
- Running or WaitingForChildren → leave it; only poll.
|
||||||
- WaitingForReview → leave it; it goes straight to Phase 4.
|
- 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.
|
Call wait_for_task_change with the ids of every task still Queued or Running (timeoutSeconds up to 170) instead of sleeping and polling get_task yourself. It returns as soon as any of them leaves Queued/Running — WaitingForReview on success, Failed on error — or reports timedOut if none did. Report progress as tasks land, then call it again with whatever ids are still Queued/Running until none remain.
|
||||||
|
|
||||||
## Phase 4 — Review and merge
|
## Phase 4 — Review and merge
|
||||||
One task at a time, in the order the brief lists them.
|
Before merging anything, call preview_merge_set with every surviving task's id (the same targetBranch you are about to merge into). It tells you, per task, whether a clean merge-tree preview is even possible (status/conflictFiles/changedFileCount/behind) and which files more than one of the tasks changed (overlaps). Read the overlaps: a file two tasks both touch is where a same-branch collision could happen. This is a HINT, not proof — it only catches same-file overlap, not a cross-file break (e.g. one task deletes a symbol another task's file still references), and a clean preview never guarantees the result compiles or passes tests. Use it to decide merge order and to know which pairs to look at extra carefully in step 1 below; it does not replace reading the diffs.
|
||||||
|
|
||||||
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.
|
One task at a time. Default to the order the brief lists them. If the overlap check above (or the file lists you gathered in Phase 2) shows two or more tasks touching the same file, tell the user which tasks collide and merge those in an order you can justify (e.g. the one making the smaller change first) — deviate from brief order only with that stated reason.
|
||||||
|
|
||||||
|
1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title and description. If preview_merge_set flagged this task in an overlap, also skim the diff of the other task(s) sharing that file.
|
||||||
2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
|
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).
|
3. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||||
- Clean merge → the task is Done; move on.
|
- Clean merge → the task is Done; move on.
|
||||||
- Conflict (markers left in the working tree, repoPath returned) → resolve it.
|
- Conflict (markers left in the working tree, repoPath returned) → resolve it.
|
||||||
|
|
||||||
|
After each merge, main has moved — a clean preview_merge_set result from before this merge is now stale for the remaining tasks. If you are unsure whether an earlier overlap warning still matters, call preview_merge for the next task again before merging it.
|
||||||
|
|
||||||
Every branch in this run forked from the same base, so conflicts between them are the NORMAL case, not a failure. Resolve them and keep going; do not abandon the run because a merge conflicted.
|
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:
|
Resolving a conflict:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ public sealed class AppSettingsRepository
|
|||||||
row.StandupWeekday = updated.StandupWeekday;
|
row.StandupWeekday = updated.StandupWeekday;
|
||||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
||||||
|
row.UsageGateFiveHourPct = Math.Clamp(updated.UsageGateFiveHourPct, 0, 100);
|
||||||
|
row.UsageGateSevenDayPct = Math.Clamp(updated.UsageGateSevenDayPct, 0, 100);
|
||||||
|
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public sealed class ListRepository
|
|||||||
existing.AgentPath = config.AgentPath;
|
existing.AgentPath = config.AgentPath;
|
||||||
existing.MaxTurns = config.MaxTurns;
|
existing.MaxTurns = config.MaxTurns;
|
||||||
existing.SessionSkills = config.SessionSkills;
|
existing.SessionSkills = config.SessionSkills;
|
||||||
|
existing.VerifyCommand = config.VerifyCommand;
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,60 @@ public sealed class TaskRepository
|
|||||||
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
||||||
=> GetByListIdAsync(listId, ct);
|
=> GetByListIdAsync(listId, ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the ids of every descendant of <paramref name="taskId"/> (children, grandchildren, ...),
|
||||||
|
/// walking the ParentTaskId chain breadth-first. Does not include the task itself.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<string>> GetDescendantIdsAsync(string taskId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
// Guards against a corrupt parent chain (a self-parent or a cycle would loop forever).
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal) { taskId };
|
||||||
|
var frontier = new List<string> { taskId };
|
||||||
|
while (frontier.Count > 0)
|
||||||
|
{
|
||||||
|
var children = await _context.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.ParentTaskId != null && frontier.Contains(t.ParentTaskId))
|
||||||
|
.Select(t => t.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var fresh = children.Where(seen.Add).ToList();
|
||||||
|
if (fresh.Count == 0) break;
|
||||||
|
result.AddRange(fresh);
|
||||||
|
frontier = fresh;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a task (and every descendant, so a child never ends up in a different list than its
|
||||||
|
/// parent) to <paramref name="targetListId"/>, appending it at the end of the target list's
|
||||||
|
/// order. ListId is init-only, so the move goes through ExecuteUpdate rather than a tracked
|
||||||
|
/// entity mutation.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MoveToListAsync(string taskId, string targetListId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var exists = await _context.Tasks.AsNoTracking().AnyAsync(t => t.Id == taskId, ct);
|
||||||
|
if (!exists)
|
||||||
|
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
var descendantIds = await GetDescendantIdsAsync(taskId, ct);
|
||||||
|
var movedIds = new List<string> { taskId };
|
||||||
|
movedIds.AddRange(descendantIds);
|
||||||
|
|
||||||
|
var maxSort = await _context.Tasks
|
||||||
|
.Where(t => t.ListId == targetListId)
|
||||||
|
.Select(t => (int?)t.SortOrder)
|
||||||
|
.MaxAsync(ct);
|
||||||
|
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => movedIds.Contains(t.Id))
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.ListId, targetListId), ct);
|
||||||
|
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SortOrder, (maxSort ?? -1) + 1), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
@@ -222,8 +276,12 @@ public sealed class TaskRepository
|
|||||||
string? commitType,
|
string? commitType,
|
||||||
string? createdBy = null,
|
string? createdBy = null,
|
||||||
string? model = null,
|
string? model = null,
|
||||||
|
int? maxTurns = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
if (maxTurns.HasValue && maxTurns.Value <= 0)
|
||||||
|
throw new ArgumentException($"maxTurns must be positive, got {maxTurns.Value}.");
|
||||||
|
|
||||||
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
||||||
// bypasses the change tracker; a tracked Find would return stale data.
|
// bypasses the change tracker; a tracked Find would return stale data.
|
||||||
var parent = await _context.Tasks.AsNoTracking()
|
var parent = await _context.Tasks.AsNoTracking()
|
||||||
@@ -249,6 +307,7 @@ public sealed class TaskRepository
|
|||||||
SortOrder = (maxSort ?? -1) + 1,
|
SortOrder = (maxSort ?? -1) + 1,
|
||||||
CreatedBy = createdBy,
|
CreatedBy = createdBy,
|
||||||
Model = ModelRegistry.NormalizeAlias(model),
|
Model = ModelRegistry.NormalizeAlias(model),
|
||||||
|
MaxTurns = maxTurns,
|
||||||
};
|
};
|
||||||
_context.Tasks.Add(child);
|
_context.Tasks.Add(child);
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
@@ -330,6 +389,19 @@ public sealed class TaskRepository
|
|||||||
.SetProperty(t => t.PlanningSessionId, sessionId), ct);
|
.SetProperty(t => t.PlanningSessionId, sessionId), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stamps the review range's head commit for a worktree-less "list handler" host task
|
||||||
|
// (Mission Control's "Let Claude handle it") when its ConPTY session is submitted for review.
|
||||||
|
public async Task SetHandlerHeadCommitAsync(
|
||||||
|
string taskId,
|
||||||
|
string headCommit,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s
|
||||||
|
.SetProperty(t => t.HandlerHeadCommit, headCommit), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TaskEntity?> FindByPlanningTokenAsync(
|
public async Task<TaskEntity?> FindByPlanningTokenAsync(
|
||||||
string token,
|
string token,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ public sealed class WorktreeRepository
|
|||||||
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetMergedAsync(string taskId, string mergeCommit, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.Worktrees
|
||||||
|
.Where(w => w.TaskId == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s
|
||||||
|
.SetProperty(w => w.State, WorktreeState.Merged)
|
||||||
|
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// The app relaunches us via ShellExecute without an explicit working directory, so we
|
||||||
|
// inherit its CWD — which the Start Menu shortcut sets to <InstallDir>\app. A process's
|
||||||
|
// current directory is locked by the OS, so we would block DownloadAndExtractStep's
|
||||||
|
// Directory.Move of app\ against ourselves (fails on every attempt, reboot included).
|
||||||
|
// Step out of the install tree before anything else touches it.
|
||||||
|
try { Environment.CurrentDirectory = Path.GetTempPath(); } catch { /* best effort */ }
|
||||||
|
|
||||||
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
|
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
|
||||||
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
||||||
var localeStore = LocaleStore.Load(localesDir);
|
var localeStore = LocaleStore.Load(localesDir);
|
||||||
@@ -124,7 +131,7 @@ public partial class App : Application
|
|||||||
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
||||||
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
||||||
// can pull them out individually via GetRequiredService<T>().
|
// can pull them out individually via GetRequiredService<T>().
|
||||||
sc.AddSingleton<DownloadAndExtractStep>();
|
sc.AddSingleton(sp => new DownloadAndExtractStep(sp.GetRequiredService<IReleaseClient>()));
|
||||||
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>());
|
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>());
|
||||||
sc.AddSingleton<IInstallStep, WriteConfigStep>();
|
sc.AddSingleton<IInstallStep, WriteConfigStep>();
|
||||||
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ Non-fatal if `claude` CLI is missing or too old (prints the manual command). Ser
|
|||||||
|
|
||||||
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
||||||
|
|
||||||
|
**`DownloadAndExtractStep`** — fetches `checksums.txt` first and only touches the install dir after the zip verifies. The zip is cached in `%TEMP%\ClaudeDo-download-cache` (ctor takes an override for tests) and reused on a retry when its SHA-256 still matches, so a failed attempt doesn't cost another full download; it is dropped after a successful install, a bad download is deleted immediately, and zips of other versions are pruned. `app\`/`worker\` are stashed to `*.bak` before extraction and restored if extraction fails.
|
||||||
|
|
||||||
|
### Gotcha: the installer must never run from inside the install dir
|
||||||
|
|
||||||
|
`App.OnStartup` sets `Environment.CurrentDirectory` to `%TEMP%`, and the UI passes an explicit `WorkingDirectory` when it relaunches us. A process's current directory is locked by Windows: inheriting the app's CWD (`<InstallDir>\app`, from the Start Menu shortcut's "start in") made the installer block its own `app` → `app.bak` rename, so every update failed with "Could not replace the existing files" — unaffected by retries or a reboot. Keep both guards.
|
||||||
|
|
||||||
## `InstallContext` Defaults
|
## `InstallContext` Defaults
|
||||||
|
|
||||||
| Property | Default |
|
| Property | Default |
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ namespace ClaudeDo.Installer.Steps;
|
|||||||
public sealed class DownloadAndExtractStep : IInstallStep
|
public sealed class DownloadAndExtractStep : IInstallStep
|
||||||
{
|
{
|
||||||
private readonly IReleaseClient _releases;
|
private readonly IReleaseClient _releases;
|
||||||
|
private readonly string _cacheDir;
|
||||||
|
|
||||||
public DownloadAndExtractStep(IReleaseClient releases)
|
public DownloadAndExtractStep(IReleaseClient releases, string? cacheDirectory = null)
|
||||||
{
|
{
|
||||||
_releases = releases;
|
_releases = releases;
|
||||||
|
// Downloads survive a failed attempt so a retry doesn't pull ~100 MB again.
|
||||||
|
// %TEMP% because Storage Sense ages the cache out on its own.
|
||||||
|
_cacheDir = cacheDirectory ?? Path.Combine(Path.GetTempPath(), "ClaudeDo-download-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Download and Extract";
|
public string Name => "Download and Extract";
|
||||||
@@ -38,38 +42,50 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
if (checksumAsset is null)
|
if (checksumAsset is null)
|
||||||
return StepResult.Fail("checksums.txt not found in release metadata.");
|
return StepResult.Fail("checksums.txt not found in release metadata.");
|
||||||
|
|
||||||
var scratchDir = Path.Combine(Path.GetTempPath(), "ClaudeDo-install-" + Guid.NewGuid().ToString("N"));
|
Directory.CreateDirectory(_cacheDir);
|
||||||
Directory.CreateDirectory(scratchDir);
|
var zipPath = Path.Combine(_cacheDir, zipAsset.Name);
|
||||||
|
var checksumPath = Path.Combine(_cacheDir, "checksums.txt");
|
||||||
|
PruneCacheExcept(zipAsset.Name);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
|
||||||
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
|
||||||
|
|
||||||
var totalMb = zipAsset.Size / (1024 * 1024);
|
|
||||||
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
|
||||||
long lastReportedMb = -1;
|
|
||||||
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
|
||||||
new Progress<long>(b =>
|
|
||||||
{
|
|
||||||
var mb = b / (1024 * 1024);
|
|
||||||
if (mb == lastReportedMb) return;
|
|
||||||
lastReportedMb = mb;
|
|
||||||
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
|
|
||||||
progress.Report($"\r {mb} / {totalMb} MB downloaded");
|
|
||||||
}),
|
|
||||||
ct);
|
|
||||||
|
|
||||||
progress.Report("Downloading checksums...");
|
progress.Report("Downloading checksums...");
|
||||||
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
||||||
new Progress<long>(_ => { }), ct);
|
new Progress<long>(_ => { }), ct);
|
||||||
|
|
||||||
progress.Report("Verifying checksum...");
|
|
||||||
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
||||||
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
||||||
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
||||||
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
|
||||||
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
// An earlier attempt may have failed after the download (locked files, bad
|
||||||
|
// extraction). Reuse that zip when it still verifies rather than re-downloading.
|
||||||
|
if (File.Exists(zipPath) && ChecksumVerifier.Verify(zipPath, expectedHash))
|
||||||
|
{
|
||||||
|
progress.Report($"Reusing the already downloaded {zipAsset.Name}.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var totalMb = zipAsset.Size / (1024 * 1024);
|
||||||
|
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
||||||
|
long lastReportedMb = -1;
|
||||||
|
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
||||||
|
new Progress<long>(b =>
|
||||||
|
{
|
||||||
|
var mb = b / (1024 * 1024);
|
||||||
|
if (mb == lastReportedMb) return;
|
||||||
|
lastReportedMb = mb;
|
||||||
|
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
|
||||||
|
progress.Report($"\r {mb} / {totalMb} MB downloaded");
|
||||||
|
}),
|
||||||
|
ct);
|
||||||
|
|
||||||
|
progress.Report("Verifying checksum...");
|
||||||
|
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
||||||
|
{
|
||||||
|
// Never keep a bad download around — it would be re-verified forever.
|
||||||
|
TryDelete(zipPath);
|
||||||
|
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only after verification do we touch the install directory.
|
// Only after verification do we touch the install directory.
|
||||||
progress.Report("Stashing previous app/worker binaries...");
|
progress.Report("Stashing previous app/worker binaries...");
|
||||||
@@ -78,20 +94,31 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
var appBak = appDest + ".bak";
|
var appBak = appDest + ".bak";
|
||||||
var workerBak = workerDest + ".bak";
|
var workerBak = workerDest + ".bak";
|
||||||
|
|
||||||
|
var stashedApp = false;
|
||||||
|
var failedPath = ctx.InstallDirectory;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
failedPath = appBak;
|
||||||
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||||
|
failedPath = workerBak;
|
||||||
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||||
if (Directory.Exists(appDest)) MoveWithRetry(appDest, appBak);
|
failedPath = appDest;
|
||||||
|
if (Directory.Exists(appDest)) { MoveWithRetry(appDest, appBak); stashedApp = true; }
|
||||||
|
failedPath = workerDest;
|
||||||
if (Directory.Exists(workerDest)) MoveWithRetry(workerDest, workerBak);
|
if (Directory.Exists(workerDest)) MoveWithRetry(workerDest, workerBak);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
// Undo a half-done stash: a leftover app.bak would be deleted as a stale
|
||||||
|
// stash on the next attempt — that copy is the only one we still have.
|
||||||
|
if (stashedApp && !Directory.Exists(appDest) && Directory.Exists(appBak))
|
||||||
|
try { MoveWithRetry(appBak, appDest); } catch { /* best effort */ }
|
||||||
|
|
||||||
// A just-stopped app/worker (or an Explorer/terminal window sitting in
|
// A just-stopped app/worker (or an Explorer/terminal window sitting in
|
||||||
// the install dir) still held a handle. Surface an actionable message
|
// the install dir) still held a handle. Surface an actionable message
|
||||||
// instead of the raw "process cannot access the file" error.
|
// instead of the raw "process cannot access the file" error.
|
||||||
return StepResult.Fail(
|
return StepResult.Fail(
|
||||||
"Could not replace the existing app/worker files — they are still in use. " +
|
$"Could not replace '{failedPath}' — it is still in use. " +
|
||||||
"Make sure ClaudeDo is fully closed (app and worker) and no Explorer or " +
|
"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}");
|
$"terminal window is open inside the install folder, then run the update again. Details: {ex.Message}");
|
||||||
}
|
}
|
||||||
@@ -117,13 +144,32 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
|||||||
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
if (Directory.Exists(appBak)) DeleteWithRetry(appBak);
|
||||||
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
if (Directory.Exists(workerBak)) DeleteWithRetry(workerBak);
|
||||||
|
|
||||||
|
// Installed — the cached zip has served its purpose.
|
||||||
|
TryDelete(zipPath);
|
||||||
|
TryDelete(checksumPath);
|
||||||
|
|
||||||
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
||||||
return StepResult.Ok();
|
return StepResult.Ok();
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
|
||||||
|
// Zips from earlier attempts on other versions would pile up otherwise.
|
||||||
|
private void PruneCacheExcept(string keepFileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try { Directory.Delete(scratchDir, recursive: true); } catch { /* best effort */ }
|
foreach (var file in Directory.EnumerateFiles(_cacheDir, "*.zip"))
|
||||||
|
{
|
||||||
|
if (!string.Equals(Path.GetFileName(file), keepFileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
TryDelete(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string file)
|
||||||
|
{
|
||||||
|
try { File.Delete(file); } catch { /* best effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MoveWithRetry(string source, string dest)
|
private static void MoveWithRetry(string source, string dest)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
"permission": "Berechtigung",
|
"permission": "Berechtigung",
|
||||||
"maxParallelExecutions": "Max. parallele Ausführungen",
|
"maxParallelExecutions": "Max. parallele Ausführungen",
|
||||||
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
|
||||||
|
"usageGateHeading": "Usage-Limit-Stopp",
|
||||||
|
"usageGateHint": "Ab dieser Auslastung holt die Queue keine neuen Tasks mehr; laufende Tasks laufen zu Ende. 0 schaltet den Stopp ab.",
|
||||||
|
"usageGateFiveHourPct": "5-Stunden-Fenster (%)",
|
||||||
|
"usageGateSevenDayPct": "7-Tage-Fenster (%)",
|
||||||
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
|
||||||
"standupWeekday": "Standup-Wochentag",
|
"standupWeekday": "Standup-Wochentag",
|
||||||
"weekdaySunday": "Sonntag",
|
"weekdaySunday": "Sonntag",
|
||||||
@@ -138,7 +142,6 @@
|
|||||||
"ctxMarkDone": "Erledigt",
|
"ctxMarkDone": "Erledigt",
|
||||||
"ctxMarkCancelled": "Abgebrochen",
|
"ctxMarkCancelled": "Abgebrochen",
|
||||||
"ctxOpenConPtySession": "ConPTY-Sitzung öffnen",
|
"ctxOpenConPtySession": "ConPTY-Sitzung öffnen",
|
||||||
"ctxPickUpInTerminal": "Im Terminal fortsetzen",
|
|
||||||
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
||||||
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
|
||||||
"ctxFinalizePlanningSession": "Plan finalisieren",
|
"ctxFinalizePlanningSession": "Plan finalisieren",
|
||||||
@@ -192,7 +195,6 @@
|
|||||||
"addReposTip": "Repos als Listen hinzufügen"
|
"addReposTip": "Repos als Listen hinzufügen"
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
"pickUpInTerminalTip": "Diese Sitzung im Terminal fortsetzen",
|
|
||||||
"deleteTaskTip": "Aufgabe löschen",
|
"deleteTaskTip": "Aufgabe löschen",
|
||||||
"killSessionTip": "Laufende Sitzung beenden",
|
"killSessionTip": "Laufende Sitzung beenden",
|
||||||
"closeTip": "Schließen",
|
"closeTip": "Schließen",
|
||||||
@@ -217,6 +219,12 @@
|
|||||||
"prepTitle": "Tagesvorbereitung",
|
"prepTitle": "Tagesvorbereitung",
|
||||||
"planDay": "Tag planen",
|
"planDay": "Tag planen",
|
||||||
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
||||||
|
"roadblockReply": {
|
||||||
|
"placeholder": "Auf den Roadblock antworten…",
|
||||||
|
"send": "Senden",
|
||||||
|
"noSession": "Keine Sitzung zum Fortsetzen — dieser Lauf hat keine Session-ID zum Antworten.",
|
||||||
|
"failed": "Antwort konnte nicht gesendet werden: {0}"
|
||||||
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"sectionLabel": "ANHÄNGE",
|
"sectionLabel": "ANHÄNGE",
|
||||||
"dropToAttach": "Zum Anhängen ablegen",
|
"dropToAttach": "Zum Anhängen ablegen",
|
||||||
@@ -266,23 +274,21 @@
|
|||||||
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "In App öffnen",
|
|
||||||
"cancel": "Abbrechen",
|
|
||||||
"detach": "Abdocken",
|
|
||||||
"redock": "Andocken",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"newSession": "Neue Sitzung",
|
"newSession": "Neue Sitzung",
|
||||||
"clearFinished": "Erledigte entfernen",
|
|
||||||
"empty": "Keine laufenden Aufgaben",
|
"empty": "Keine laufenden Aufgaben",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"queue": "Warteschlange",
|
"queue": "Warteschlange",
|
||||||
"blocked": "Blockiert",
|
"blocked": "Blockiert",
|
||||||
|
"running": "Läuft",
|
||||||
"focusMode": "Fokus",
|
"focusMode": "Fokus",
|
||||||
"overviewMode": "Übersicht",
|
"overviewMode": "Übersicht",
|
||||||
"closeSession": "Sitzung schließen",
|
"closeSession": "Sitzung schließen",
|
||||||
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
||||||
"conptyStarting": "Sitzung wird gestartet…",
|
"conptyStarting": "Sitzung wird gestartet…",
|
||||||
"mergeHelperTitle": "Merge-Helfer",
|
"mergeHelperTitle": "Merge-Helfer",
|
||||||
|
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
||||||
|
"mergeHelperTaskDescriptionHeader": "Von diesem Lauf bearbeitete Tasks:",
|
||||||
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
||||||
"submitForReview": "Zum Review einreichen",
|
"submitForReview": "Zum Review einreichen",
|
||||||
"submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann",
|
"submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann",
|
||||||
@@ -330,7 +336,11 @@
|
|||||||
"manualList": "Manuelle Liste (Erinnerungen)",
|
"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.",
|
"manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen"
|
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
||||||
|
"sectionVerify": "VERIFIKATION",
|
||||||
|
"verifyCommand": "Verifikationskommando",
|
||||||
|
"verifyCommandPlaceholder": "(keines — Merge verhält sich wie bisher)",
|
||||||
|
"verifyCommandHint": "Läuft im Arbeitsverzeichnis nach einem Merge, bevor die Aufgabe auf 'Erledigt' gesetzt wird. Ein Exit-Code ungleich 0 (oder Timeout) lässt den Merge bestehen, verhindert aber 'Erledigt' und meldet den Fehler."
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
"title": "WORKTREE MERGEN",
|
"title": "WORKTREE MERGEN",
|
||||||
@@ -412,6 +422,39 @@
|
|||||||
"generate": "Erstellen",
|
"generate": "Erstellen",
|
||||||
"regenerate": "Neu erstellen",
|
"regenerate": "Neu erstellen",
|
||||||
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
|
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
|
||||||
|
},
|
||||||
|
"usageMonitor": {
|
||||||
|
"title": "USAGE MONITOR",
|
||||||
|
"windowTitle": "Usage Monitor",
|
||||||
|
"noGauges": "Noch keine Nutzungslimits gemeldet.",
|
||||||
|
"staleFormat": "Werte veraltet (Stand {0})",
|
||||||
|
"staleGateHint": "Das Gate greift in diesem Zustand nicht.",
|
||||||
|
"gateBlockedFormat": "Queue pausiert — {0}",
|
||||||
|
"resetIn": "Reset in {0}",
|
||||||
|
"gaugeSession": "Session (5 Std.)",
|
||||||
|
"gaugeWeeklyAll": "Woche (alle Modelle)",
|
||||||
|
"gaugeWeeklyScopedFormat": "Woche ({0})",
|
||||||
|
"period": "Zeitraum:",
|
||||||
|
"last7Days": "7 Tage",
|
||||||
|
"last30Days": "30 Tage",
|
||||||
|
"fromDate": "Von",
|
||||||
|
"toDate": "Bis",
|
||||||
|
"modelUsage": "Modelle",
|
||||||
|
"taskUsage": "Tasks",
|
||||||
|
"emptyStateHint": "Noch keine Nutzungsdaten für diesen Zeitraum.",
|
||||||
|
"columnModel": "MODELL",
|
||||||
|
"columnIn": "IN",
|
||||||
|
"columnOut": "OUT",
|
||||||
|
"columnCache": "CACHE",
|
||||||
|
"columnOtherIn": "ÜBRIGES IN",
|
||||||
|
"columnOtherOut": "ÜBRIGES OUT",
|
||||||
|
"columnOtherCache": "ÜBRIGES CACHE",
|
||||||
|
"columnShare": "ANTEIL",
|
||||||
|
"columnTask": "TASK",
|
||||||
|
"columnList": "LISTE",
|
||||||
|
"columnModelShort": "MODELL",
|
||||||
|
"columnRuns": "RUNS",
|
||||||
|
"columnTotal": "GESAMT"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installer": {
|
"installer": {
|
||||||
@@ -525,6 +568,7 @@
|
|||||||
"restartWorker": "Worker neu starten",
|
"restartWorker": "Worker neu starten",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Wochenbericht…",
|
"weeklyReport": "Wochenbericht…",
|
||||||
|
"usageMonitor": "Usage Monitor…",
|
||||||
"about": "Über…",
|
"about": "Über…",
|
||||||
"addRepos": "Repos als Listen hinzufügen…"
|
"addRepos": "Repos als Listen hinzufügen…"
|
||||||
},
|
},
|
||||||
@@ -541,19 +585,37 @@
|
|||||||
"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" },
|
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" },
|
||||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "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}" },
|
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar." },
|
||||||
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
|
||||||
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." },
|
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien).", "buildFailed": "Kombinierte Vorschau konnte nicht erstellt werden: {0}" },
|
||||||
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||||
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
|
||||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
||||||
|
"usageMonitor": { "loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}" },
|
||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
|
||||||
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen 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." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "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." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." },
|
||||||
"listSettings": { "untitled": "Unbenannt" },
|
"listSettings": { "untitled": "Unbenannt" },
|
||||||
|
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt." },
|
||||||
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"pill": {
|
||||||
|
"empty": "Nutzung –",
|
||||||
|
"bothFormat": "5h {0}% · 7d {1}%",
|
||||||
|
"fiveHourFormat": "5h {0}%",
|
||||||
|
"sevenDayFormat": "7d {0}%",
|
||||||
|
"fiveHourLabel": "5h",
|
||||||
|
"sevenDayLabel": "7d",
|
||||||
|
"resetIn": "{0}: Reset in {1}",
|
||||||
|
"durationHoursMinutes": "{0} h {1} m",
|
||||||
|
"durationMinutes": "{0} m",
|
||||||
|
"blockedReason": "Blockiert: {0}",
|
||||||
|
"stale": "veraltet (Stand {0})",
|
||||||
|
"lastError": "Letzter Fehler: {0}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
"permission": "Permission",
|
"permission": "Permission",
|
||||||
"maxParallelExecutions": "Max parallel executions",
|
"maxParallelExecutions": "Max parallel executions",
|
||||||
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
|
||||||
|
"usageGateHeading": "Usage limit stop",
|
||||||
|
"usageGateHint": "Once usage reaches this level, the queue stops picking up new tasks; running tasks finish normally. 0 turns the stop off.",
|
||||||
|
"usageGateFiveHourPct": "5-hour window (%)",
|
||||||
|
"usageGateSevenDayPct": "7-day window (%)",
|
||||||
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
"reportExcludedPaths": "Report: excluded paths (one per line)",
|
||||||
"standupWeekday": "Standup weekday",
|
"standupWeekday": "Standup weekday",
|
||||||
"weekdaySunday": "Sunday",
|
"weekdaySunday": "Sunday",
|
||||||
@@ -138,7 +142,6 @@
|
|||||||
"ctxMarkDone": "Done",
|
"ctxMarkDone": "Done",
|
||||||
"ctxMarkCancelled": "Cancelled",
|
"ctxMarkCancelled": "Cancelled",
|
||||||
"ctxOpenConPtySession": "Open ConPTY session",
|
"ctxOpenConPtySession": "Open ConPTY session",
|
||||||
"ctxPickUpInTerminal": "Pick up in terminal",
|
|
||||||
"ctxOpenPlanningSession": "Open planning Session",
|
"ctxOpenPlanningSession": "Open planning Session",
|
||||||
"ctxResumePlanningSession": "Resume planning Session",
|
"ctxResumePlanningSession": "Resume planning Session",
|
||||||
"ctxFinalizePlanningSession": "Finalize plan",
|
"ctxFinalizePlanningSession": "Finalize plan",
|
||||||
@@ -192,7 +195,6 @@
|
|||||||
"addReposTip": "Add repos as lists"
|
"addReposTip": "Add repos as lists"
|
||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
"pickUpInTerminalTip": "Pick up this session in a terminal",
|
|
||||||
"deleteTaskTip": "Delete task",
|
"deleteTaskTip": "Delete task",
|
||||||
"killSessionTip": "Kill the running session",
|
"killSessionTip": "Kill the running session",
|
||||||
"closeTip": "Close",
|
"closeTip": "Close",
|
||||||
@@ -217,6 +219,12 @@
|
|||||||
"prepTitle": "Daily prep",
|
"prepTitle": "Daily prep",
|
||||||
"planDay": "Plan day",
|
"planDay": "Plan day",
|
||||||
"prepEmpty": "No prep run today yet — click Plan day",
|
"prepEmpty": "No prep run today yet — click Plan day",
|
||||||
|
"roadblockReply": {
|
||||||
|
"placeholder": "Reply to the roadblock…",
|
||||||
|
"send": "Send",
|
||||||
|
"noSession": "No session to resume — this run has no session ID to reply into.",
|
||||||
|
"failed": "Couldn't send the reply: {0}"
|
||||||
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"sectionLabel": "ATTACHMENTS",
|
"sectionLabel": "ATTACHMENTS",
|
||||||
"dropToAttach": "Drop to attach",
|
"dropToAttach": "Drop to attach",
|
||||||
@@ -266,23 +274,21 @@
|
|||||||
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
"reviewResetTip": "Discard all changes and reset the task to Idle"
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
"openInApp": "Open in app",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"detach": "Detach",
|
|
||||||
"redock": "Re-dock",
|
|
||||||
"windowTitle": "Mission Control",
|
"windowTitle": "Mission Control",
|
||||||
"newSession": "New session",
|
"newSession": "New session",
|
||||||
"clearFinished": "Clear finished",
|
|
||||||
"empty": "No running tasks",
|
"empty": "No running tasks",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"blocked": "Blocked",
|
"blocked": "Blocked",
|
||||||
|
"running": "Running",
|
||||||
"focusMode": "Focus",
|
"focusMode": "Focus",
|
||||||
"overviewMode": "Overview",
|
"overviewMode": "Overview",
|
||||||
"closeSession": "Close session",
|
"closeSession": "Close session",
|
||||||
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
||||||
"conptyStarting": "Starting session…",
|
"conptyStarting": "Starting session…",
|
||||||
"mergeHelperTitle": "Merge Helper",
|
"mergeHelperTitle": "Merge Helper",
|
||||||
|
"mergeHelperTaskTitle": "List handler: {0}",
|
||||||
|
"mergeHelperTaskDescriptionHeader": "Tasks handled by this run:",
|
||||||
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
||||||
"submitForReview": "Submit for review",
|
"submitForReview": "Submit for review",
|
||||||
"submitForReviewTip": "Commit this worktree and move the task to review so it can be merged",
|
"submitForReviewTip": "Commit this worktree and move the task to review so it can be merged",
|
||||||
@@ -330,7 +336,11 @@
|
|||||||
"manualList": "Manual list (reminders)",
|
"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.",
|
"manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Reset agent settings"
|
"resetAgentSettings": "Reset agent settings",
|
||||||
|
"sectionVerify": "VERIFICATION",
|
||||||
|
"verifyCommand": "Verify command",
|
||||||
|
"verifyCommandPlaceholder": "(none — merge behaves as today)",
|
||||||
|
"verifyCommandHint": "Runs in the working directory after a merge lands, before the task is marked Done. A non-zero exit (or timeout) leaves the merge in place but keeps the task out of Done and reports the failure."
|
||||||
},
|
},
|
||||||
"merge": {
|
"merge": {
|
||||||
"title": "MERGE WORKTREE",
|
"title": "MERGE WORKTREE",
|
||||||
@@ -412,6 +422,39 @@
|
|||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"emptyStateHint": "No report for this range yet. Click “Generate”."
|
"emptyStateHint": "No report for this range yet. Click “Generate”."
|
||||||
|
},
|
||||||
|
"usageMonitor": {
|
||||||
|
"title": "USAGE MONITOR",
|
||||||
|
"windowTitle": "Usage Monitor",
|
||||||
|
"noGauges": "No usage limits reported yet.",
|
||||||
|
"staleFormat": "Values stale (as of {0})",
|
||||||
|
"staleGateHint": "The gate does not apply while values are stale.",
|
||||||
|
"gateBlockedFormat": "Queue paused — {0}",
|
||||||
|
"resetIn": "Reset in {0}",
|
||||||
|
"gaugeSession": "Session (5h)",
|
||||||
|
"gaugeWeeklyAll": "Week (all models)",
|
||||||
|
"gaugeWeeklyScopedFormat": "Week ({0})",
|
||||||
|
"period": "Period:",
|
||||||
|
"last7Days": "7 days",
|
||||||
|
"last30Days": "30 days",
|
||||||
|
"fromDate": "From",
|
||||||
|
"toDate": "To",
|
||||||
|
"modelUsage": "Models",
|
||||||
|
"taskUsage": "Tasks",
|
||||||
|
"emptyStateHint": "No usage data for this range yet.",
|
||||||
|
"columnModel": "MODEL",
|
||||||
|
"columnIn": "IN",
|
||||||
|
"columnOut": "OUT",
|
||||||
|
"columnCache": "CACHE",
|
||||||
|
"columnOtherIn": "OTHER IN",
|
||||||
|
"columnOtherOut": "OTHER OUT",
|
||||||
|
"columnOtherCache": "OTHER CACHE",
|
||||||
|
"columnShare": "SHARE",
|
||||||
|
"columnTask": "TASK",
|
||||||
|
"columnList": "LIST",
|
||||||
|
"columnModelShort": "MODEL",
|
||||||
|
"columnRuns": "RUNS",
|
||||||
|
"columnTotal": "TOTAL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installer": {
|
"installer": {
|
||||||
@@ -525,6 +568,7 @@
|
|||||||
"restartWorker": "Restart worker",
|
"restartWorker": "Restart worker",
|
||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Weekly Report…",
|
"weeklyReport": "Weekly Report…",
|
||||||
|
"usageMonitor": "Usage Monitor…",
|
||||||
"about": "About…",
|
"about": "About…",
|
||||||
"addRepos": "Add repos as lists…"
|
"addRepos": "Add repos as lists…"
|
||||||
},
|
},
|
||||||
@@ -541,19 +585,37 @@
|
|||||||
"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" },
|
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" },
|
||||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "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}" },
|
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available." },
|
||||||
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
|
||||||
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." },
|
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files).", "buildFailed": "Could not build combined preview: {0}" },
|
||||||
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||||
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
||||||
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
||||||
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
|
||||||
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
||||||
|
"usageMonitor": { "loadFailed": "Couldn't load usage data: {0}" },
|
||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
|
||||||
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove 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)." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "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." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." },
|
||||||
"listSettings": { "untitled": "Untitled" },
|
"listSettings": { "untitled": "Untitled" },
|
||||||
|
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done." },
|
||||||
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"pill": {
|
||||||
|
"empty": "Usage –",
|
||||||
|
"bothFormat": "5h {0}% · 7d {1}%",
|
||||||
|
"fiveHourFormat": "5h {0}%",
|
||||||
|
"sevenDayFormat": "7d {0}%",
|
||||||
|
"fiveHourLabel": "5h",
|
||||||
|
"sevenDayLabel": "7d",
|
||||||
|
"resetIn": "{0}: Reset in {1}",
|
||||||
|
"durationHoursMinutes": "{0} h {1} m",
|
||||||
|
"durationMinutes": "{0} m",
|
||||||
|
"blockedReason": "Blocked: {0}",
|
||||||
|
"stale": "stale (as of {0})",
|
||||||
|
"lastError": "Last error: {0}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,18 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
|
|||||||
|
|
||||||
- **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`.
|
- **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`.
|
||||||
- **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`.
|
- **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`.
|
||||||
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `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.
|
- **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RefineTask`, `OpenConPtySessionRequested` (embedded ConPTY terminal), `ToggleManual` (per-task manual flag) and `SyncInteractiveSessions` (mirrors Mission Control's open ConPTY panes onto the rows); MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell.
|
||||||
- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards), the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, 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.
|
- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards) — the ROADBLOCK card also has a reply field (`RoadblockReplyDraft`/`SendRoadblockReplyCommand`, gated by `CanReplyToRoadblock` on `LatestRunSessionId`) that resumes the session via the same `ContinueTaskAsync` transport as `ContinueCommand` but with the user's own text instead of the fixed re-run prompt; failures raise `ErrorReported`, wired by the shell into `FlashFooterError`, the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, call `ShowDiffViewer`, and fire the `DiffViewed` callback; `HasReviewableDiff` reports whether anything is inspectable, feeding the review gate), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection<AttachmentRowViewModel>`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file.
|
||||||
- **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; 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`).
|
- **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; plus `IsManual` (→ MANUAL badge; suppresses `CanSendToQueue`/`CanRefine`/`CanOpenPlanningSession`) and `HasInteractiveSession` (→ accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane); list row: kind Smart/Virtual/User, count, icon/dot keys, drop hints, `IsManual`).
|
||||||
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
|
||||||
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows). 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`).
|
- **UsagePillViewModel** — one shared instance backs the `UsagePill` control hosted in both the footer and the Mission Control header; loads via `GetUsageSnapshotAsync` and updates live off `IWorkerClient.UsageUpdatedEvent`; derives display text, tooltip, and dot state (normal/warn/stale/blocked, mutually exclusive priority blocked > stale > warn > normal) from the shared `UsageSnapshotDto`.
|
||||||
|
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows). General hosts the per-model preset table (`ModelPresets` → `ModelPresetRowViewModel`: effort + max turns per alias) which **replaced** the single global "Max turns" field, `ListSettingsModalViewModel` (name, working dir, commit type, "manual list" flag, `VerifyCommand` (optional post-merge verify gate, own field/section — not part of `AgentConfigEditorViewModel`), delete list; hosts shared `AgentConfigEditorViewModel` as `Agent` property (scope=List) — save delegates to `Agent.SaveAsync(verifyCommand)`, since both fields land in the same `list_config` row via one `UpdateListConfig` call and would otherwise clobber each other), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `MergeHelperSelectionModalViewModel` ("Let Claude handle it": checkbox picker over one list's non-terminal, non-manual tasks, pre-ticks the actionable ones; list-scoped only — `Configure(listId, listName)`, no global scope. Opened from the list row's context menu, which is hidden when the list has no working dir; on confirm `ListsIslandViewModel` raises `LetClaudeHandleRequested` → shell → `MissionControlViewModel.OpenMergeHelperConPtySessionAsync`, which first calls `IWorkerClient.CreateMergeHelperTaskAsync` to create one new ClaudeDo task per run in that list — `Idle`/`IsManual=true` (never queued), title/description localized (`missionControl.mergeHelperTaskTitle`/`mergeHelperTaskDescriptionHeader`), `TaskEntity.HandlerBaseCommit` stamped to the list repo's current HEAD — then opens a **task-based** ConPTY tile for it (deduped by `TaskId` like `OpenConPtySessionAsync`, not `CreateAdHoc`) running the five-phase handler prompt. The handler still merges the tasks it handles itself; the host task never gets a worktree of its own, so "Submit for review" stamps `HandlerHeadCommit` instead of committing a worktree, and the detail pane's `MergeSectionViewModel` falls back to `HandlerBaseCommit`/`HandlerHeadCommit` over the list's working dir for its diff — see `TaskEntity` in `ClaudeDo.Data/CLAUDE.md`), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`, `LogVisualizerViewModel` (worker logs, last 30 min, all levels + a warn/error-only filter; loads via `GetRecentLogsAsync`), `UsageMonitorModalViewModel` (opened from the usage pill; renders one gauge per row in `UsageSnapshotDto.Limits` — **dynamic**, since the fixed `seven_day_opus`/`seven_day_sonnet`-style buckets the raw Anthropic API can return are plan-dependent and come back `null` on plans that don't have them, so a fixed gauge layout would break; also shows model usage (`GetModelUsageAsync`, ClaudeDo-vs-Other split per model) and top-task usage (`GetTaskUsageAsync`) tables over a 7d/30d preset or custom date range).
|
||||||
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModels.cs` holds shared types: `DiffLineViewModel`, `DiffFileViewModel`, `DiffLineKind`, `DiffFileStatus`, `SubtaskDiffRow`, `DiffTreeNodeViewModel`, `DiffTree`. `DiffViewerViewModel` is a single unified read-only diff viewer with two modes: **Files** (dirty worktree / branch-vs-base / commit-range — loads via GitService, shows a folder file-tree on the left + per-file diff pane on the right, Merge button for live branch source) and **Planning** (per-subtask diffs via `GetPlanningAggregateAsync`, subtask list left + flat diff right, combined integration-branch toggle). The Merge button opens the merge form, which routes to `ConflictResolverViewModel` on conflict. `DiffLinesView` renders per-file diff content with binary/empty placeholders.
|
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModels.cs` holds shared types: `DiffLineViewModel`, `DiffFileViewModel`, `DiffLineKind`, `DiffFileStatus`, `SubtaskDiffRow`, `DiffTreeNodeViewModel`, `DiffTree`. `DiffViewerViewModel` is a single unified read-only diff viewer with two modes: **Files** (dirty worktree / branch-vs-base / commit-range — loads via GitService, shows a folder file-tree on the left + per-file diff pane on the right, Merge button for live branch source) and **Planning** (per-subtask diffs via `GetPlanningAggregateAsync`, subtask list left + flat diff right, combined integration-branch toggle). The Merge button opens the merge form, which routes to `ConflictResolverViewModel` on conflict. `DiffLinesView` renders per-file diff content with binary/empty placeholders.
|
||||||
- **Conflicts** — `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). The editor is reached from review **Approve** on conflict and from the **Merge** button in the Diff window (a conflicting `MergeTask` hands off to the resolver via `RequestConflictResolution`).
|
- **Conflicts** — `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). The editor is reached from review **Approve** on conflict and from the **Merge** button in the Diff window (a conflicting `MergeTask` hands off to the resolver via `RequestConflictResolution`).
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, 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`.
|
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface tracks `WorkerHub` (see `src/ClaudeDo.Worker/CLAUDE.md` for the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (`ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto`, reject-to-queue/idle, cancel review, `PreviewMergeAsync -> MergePreviewDto`), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, refine), embedded ConPTY launch specs (`GetInteractiveLaunchSpecAsync`/`GetAdHocLaunchSpecAsync`), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (`RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`), prime schedules, recent worker logs (`GetRecentLogsAsync`), usage monitoring (`GetUsageSnapshotAsync -> UsageSnapshotDto?`, `GetModelUsageAsync(from, to)`, `GetTaskUsageAsync(from, to)`). Events mirror `HubBroadcaster` (task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log, `UsageUpdatedEvent`). Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`.
|
||||||
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
- **INotesApi** / **WorkerNotesApi** — daily-note CRUD (`ListAsync(day)`, `AddAsync`, `UpdateAsync`, `DeleteAsync`); UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||||
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
- **IPrimeScheduleApi** — prime-schedule CRUD (`ListAsync`, `UpsertAsync`, `DeleteAsync`).
|
||||||
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
- **UpdateCheckService** — polls releases, exposes `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` (feeds the shell's update banner).
|
||||||
@@ -66,6 +67,7 @@ Modals use `TaskCompletionSource` results behind the reusable `ModalShell` contr
|
|||||||
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
||||||
- Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible.
|
- Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible.
|
||||||
- Window key bindings live on `MainWindow`: `Ctrl+K` focuses search, `Ctrl+N` the add-task box. Do **not** bind bare punctuation gestures — `OemQuestion` used to hold search focus and silently swallowed `#` app-wide on a German layout.
|
- Window key bindings live on `MainWindow`: `Ctrl+K` focuses search, `Ctrl+N` the add-task box. Do **not** bind bare punctuation gestures — `OemQuestion` used to hold search focus and silently swallowed `#` app-wide on a German layout.
|
||||||
|
- `FocusClearing` also clears focus from a TextBox on Escape, mirroring its click-outside behavior — but the KeyDown handler is scoped to `MainWindow` specifically (`AddClassHandler<MainWindow>`, not `<TopLevel>`). Modal windows (`AboutModalView` etc.) each bind their own `Window.KeyBindings` Escape → close; since modals are separate `Window` instances, this handler never runs there, so Escape still closes them unchanged. Mission Control's ConPTY tiles (`InteractiveTerminalView`) live in `MissionControlWindow`, also unaffected — Escape always reaches the PTY there.
|
||||||
- `Ellipse.spinner` (IslandStyles) is the shared indeterminate spinner: used for a starting ConPTY pane (`InteractiveTerminalViewModel.IsStarting`) and in place of the refine button while `TaskRowViewModel.IsRefining`.
|
- `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.
|
- `ConPtyPaneViewModel` resolves its own launch spec (ctor takes a descriptor **factory**; the host wires handlers and then calls `Start()`), so the Mission Control tile appears immediately with its spinner while the worker is still preparing the worktree. A failed launch keeps the tile with its inline error banner instead of never appearing.
|
||||||
- `SessionTerminalView` is the reusable log terminal (StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`) used for both the task `Log` and the prep `PrepLog`.
|
- `SessionTerminalView` is the reusable log terminal (StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`) used for both the task `Log` and the prep `PrepLog`.
|
||||||
|
|||||||
@@ -693,6 +693,10 @@
|
|||||||
<Setter Property="CornerRadius" Value="8" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
<Setter Property="Cursor" Value="Hand" />
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<!-- Reserved so the task-drop highlight below only swaps the brush — a thickness that changes
|
||||||
|
on hover would nudge the row's content while dragging over it. -->
|
||||||
|
<Setter Property="BorderThickness" Value="1.5" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Border.list-item:pointerover">
|
<Style Selector="Border.list-item:pointerover">
|
||||||
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
|
||||||
@@ -700,6 +704,11 @@
|
|||||||
<Style Selector="Border.list-item.active">
|
<Style Selector="Border.list-item.active">
|
||||||
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!-- Highlighted while a dragged task hovers over this row as a move target. -->
|
||||||
|
<Style Selector="Border.list-item.drop-target">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
<!-- Active item text / icon colors -->
|
<!-- Active item text / icon colors -->
|
||||||
<Style Selector="Border.list-item.active TextBlock.list-label">
|
<Style Selector="Border.list-item.active TextBlock.list-label">
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
|
using ClaudeDo.Ui.Views;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears focus from a TextBox when the user clicks outside of any text box, so input
|
/// Clears focus from a TextBox when the user clicks outside of any text box, so input
|
||||||
/// fields behave like the user expects. Registered once for every window in the app.
|
/// fields behave like the user expects. Registered once for every window in the app.
|
||||||
|
/// Escape does the same, but only in the main window: modal windows bind Escape to closing
|
||||||
|
/// themselves, and Mission Control's ConPTY tiles need Escape to reach the terminal, so this
|
||||||
|
/// intentionally does not use a generic TopLevel handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FocusClearing
|
public static class FocusClearing
|
||||||
{
|
{
|
||||||
@@ -16,6 +20,9 @@ public static class FocusClearing
|
|||||||
{
|
{
|
||||||
InputElement.PointerPressedEvent.AddClassHandler<TopLevel>(
|
InputElement.PointerPressedEvent.AddClassHandler<TopLevel>(
|
||||||
OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||||
|
|
||||||
|
InputElement.KeyDownEvent.AddClassHandler<MainWindow>(
|
||||||
|
OnKeyDown, RoutingStrategies.Tunnel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e)
|
private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e)
|
||||||
@@ -31,4 +38,19 @@ public static class FocusClearing
|
|||||||
|
|
||||||
focusManager.Focus(null);
|
focusManager.Focus(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnKeyDown(MainWindow window, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key != Key.Escape)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (window.FocusManager is not { } focusManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (focusManager.GetFocusedElement() is not TextBox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
focusManager.Focus(null);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
@@ -18,6 +16,7 @@ public interface IDialogService
|
|||||||
{
|
{
|
||||||
Task ShowAboutAsync(AboutModalViewModel vm);
|
Task ShowAboutAsync(AboutModalViewModel vm);
|
||||||
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
||||||
|
Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm);
|
||||||
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
||||||
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
||||||
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
||||||
@@ -37,7 +36,4 @@ public interface IDialogService
|
|||||||
|
|
||||||
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
||||||
void ShowMissionControl(MissionControlViewModel vm);
|
void ShowMissionControl(MissionControlViewModel vm);
|
||||||
|
|
||||||
/// <summary>Show a detached monitor in its own window; <paramref name="onClosed"/> re-docks it when that window closes.</summary>
|
|
||||||
void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
||||||
Task AbortConflictMergeAsync(string taskId);
|
Task AbortConflictMergeAsync(string taskId);
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
// 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
|
/// <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>
|
/// (the only path that flips a hand-driven ConPTY session into the review/merge pipeline).</summary>
|
||||||
Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default);
|
Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default);
|
||||||
@@ -88,6 +86,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
/// <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>
|
/// 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);
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
||||||
|
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
||||||
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
||||||
|
Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default);
|
||||||
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
||||||
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
||||||
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||||
@@ -137,4 +139,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
||||||
Task SetOnlineInboxAuthAsync(string refreshToken);
|
Task SetOnlineInboxAuthAsync(string refreshToken);
|
||||||
Task ClearOnlineInboxAuthAsync();
|
Task ClearOnlineInboxAuthAsync();
|
||||||
|
|
||||||
|
/// <summary>Raised whenever the worker's usage poller ticks (success or failure).</summary>
|
||||||
|
event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||||
|
Task<UsageSnapshotDto?> GetUsageSnapshotAsync();
|
||||||
|
Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to);
|
||||||
|
Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public event Action<string>? RefineStartedEvent;
|
public event Action<string>? RefineStartedEvent;
|
||||||
public event Action<string, bool, string?>? RefineFinishedEvent;
|
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||||
|
|
||||||
|
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||||
|
|
||||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||||
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
||||||
@@ -202,6 +204,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
|
||||||
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
|
||||||
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
|
||||||
|
|
||||||
|
_hub.On<UsageSnapshotDto>("UsageUpdated", snapshot =>
|
||||||
|
Dispatcher.UIThread.Post(() => UsageUpdatedEvent?.Invoke(snapshot)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync()
|
public Task StartAsync()
|
||||||
@@ -329,8 +334,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("RefreshAgents");
|
await _hub.InvokeAsync("RefreshAgents");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||||
=> TryInvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
=> await _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||||
|
|
||||||
private async Task SeedActiveTasksAsync()
|
private async Task SeedActiveTasksAsync()
|
||||||
{
|
{
|
||||||
@@ -372,8 +377,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
||||||
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
||||||
|
|
||||||
public Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
||||||
=> TryInvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
=> await _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
||||||
|
|
||||||
public async Task DeletePrimeScheduleAsync(Guid id)
|
public async Task DeletePrimeScheduleAsync(Guid id)
|
||||||
{
|
{
|
||||||
@@ -400,8 +405,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
||||||
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
||||||
|
|
||||||
public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
||||||
=> TryInvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
=> await _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
||||||
|
|
||||||
public async Task UpdateDailyNoteAsync(string id, string text)
|
public async Task UpdateDailyNoteAsync(string id, string text)
|
||||||
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
||||||
@@ -474,11 +479,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("CancelReview", taskId);
|
await _hub.InvokeAsync("CancelReview", taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||||
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
=> await _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||||
|
|
||||||
public Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||||
=> TryInvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
=> await _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||||
|
|
||||||
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
||||||
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
||||||
@@ -501,8 +506,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
||||||
=> TryInvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
=> await _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
||||||
|
|
||||||
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
||||||
@@ -510,9 +515,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
||||||
|
|
||||||
public async Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default)
|
|
||||||
=> await _hub.InvokeAsync("ResumeTaskInTerminal", taskId, ct);
|
|
||||||
|
|
||||||
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
|
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct);
|
=> await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct);
|
||||||
|
|
||||||
@@ -525,6 +527,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
||||||
|
|
||||||
|
public async Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
=> await _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct);
|
||||||
|
|
||||||
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
||||||
|
|
||||||
@@ -543,8 +549,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||||
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
||||||
|
|
||||||
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||||
=> TryInvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
=> await _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||||
|
|
||||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||||
{
|
{
|
||||||
@@ -573,6 +579,15 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task ClearOnlineInboxAuthAsync()
|
public async Task ClearOnlineInboxAuthAsync()
|
||||||
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
||||||
|
|
||||||
|
public Task<UsageSnapshotDto?> GetUsageSnapshotAsync()
|
||||||
|
=> TryInvokeAsync<UsageSnapshotDto>("GetUsageSnapshot");
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
|
||||||
|
=> await TryInvokeAsync<List<ModelUsageRowDto>>("GetModelUsage", from, to) ?? [];
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
|
||||||
|
=> await TryInvokeAsync<List<TaskUsageRowDto>>("GetTaskUsage", from, to) ?? [];
|
||||||
|
|
||||||
// IWorkerClient explicit implementations (drop typed return values)
|
// IWorkerClient explicit implementations (drop typed return values)
|
||||||
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
||||||
=> await StartPlanningSessionAsync(taskId, ct);
|
=> await StartPlanningSessionAsync(taskId, ct);
|
||||||
@@ -608,7 +623,9 @@ public sealed record AppSettingsDto(
|
|||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
int DailyPrepMaxTasks,
|
||||||
List<string>? SessionSkills = null,
|
List<string>? SessionSkills = null,
|
||||||
List<ModelPresetDto>? ModelPresets = null);
|
List<ModelPresetDto>? ModelPresets = null,
|
||||||
|
int UsageGateFiveHourPct = 80,
|
||||||
|
int UsageGateSevenDayPct = 90);
|
||||||
|
|
||||||
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
||||||
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
@@ -629,9 +646,9 @@ public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDoc
|
|||||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||||
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false);
|
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false);
|
||||||
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public sealed record SeedResultDto(int Copied, int Skipped);
|
public sealed record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public sealed record WorktreeOverviewDto(
|
public sealed record WorktreeOverviewDto(
|
||||||
@@ -675,3 +692,46 @@ public sealed record OnlineInboxConfigInputDto(
|
|||||||
string ClientId,
|
string ClientId,
|
||||||
string Scopes,
|
string Scopes,
|
||||||
string RedirectUri);
|
string RedirectUri);
|
||||||
|
|
||||||
|
public sealed record UsageLimitDto(
|
||||||
|
string Kind,
|
||||||
|
string Group,
|
||||||
|
double Percent,
|
||||||
|
string Severity,
|
||||||
|
DateTimeOffset? ResetsAt,
|
||||||
|
string? ScopeModelDisplayName,
|
||||||
|
bool IsActive);
|
||||||
|
|
||||||
|
public sealed record UsageSnapshotDto(
|
||||||
|
double? FiveHourPercent,
|
||||||
|
DateTimeOffset? FiveHourResetsAt,
|
||||||
|
double? SevenDayPercent,
|
||||||
|
DateTimeOffset? SevenDayResetsAt,
|
||||||
|
IReadOnlyList<UsageLimitDto> Limits,
|
||||||
|
int FiveHourThresholdPct,
|
||||||
|
int SevenDayThresholdPct,
|
||||||
|
bool IsGateBlocked,
|
||||||
|
string? GateReason,
|
||||||
|
DateTime? FetchedAtUtc,
|
||||||
|
bool IsStale,
|
||||||
|
string? LastError);
|
||||||
|
|
||||||
|
public sealed record ModelUsageRowDto(
|
||||||
|
DateOnly Date,
|
||||||
|
string Model,
|
||||||
|
string Scope,
|
||||||
|
long InputTokens,
|
||||||
|
long OutputTokens,
|
||||||
|
long CacheReadTokens,
|
||||||
|
long CacheCreationTokens,
|
||||||
|
int Messages);
|
||||||
|
|
||||||
|
public sealed record TaskUsageRowDto(
|
||||||
|
string TaskId,
|
||||||
|
string TaskTitle,
|
||||||
|
string ListId,
|
||||||
|
string ListName,
|
||||||
|
string? Model,
|
||||||
|
int Runs,
|
||||||
|
long TokensIn,
|
||||||
|
long TokensOut);
|
||||||
|
|||||||
@@ -184,7 +184,10 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async System.Threading.Tasks.Task SaveAsync()
|
// verifyCommand is a List-only field owned by ListSettingsModalViewModel (not this editor,
|
||||||
|
// which is also reused for Task scope); the caller passes it through so the single
|
||||||
|
// UpdateListConfig call carries the full desired row instead of clobbering it.
|
||||||
|
public async System.Threading.Tasks.Task SaveAsync(string? verifyCommand = null)
|
||||||
{
|
{
|
||||||
if (TargetId is null) return;
|
if (TargetId is null) return;
|
||||||
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
||||||
@@ -196,7 +199,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
if (_scope == AgentConfigScope.Task)
|
if (_scope == AgentConfigScope.Task)
|
||||||
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills));
|
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills));
|
||||||
else
|
else
|
||||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills));
|
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string>? SelectedSessionSkillNames()
|
private List<string>? SelectedSessionSkillNames()
|
||||||
|
|||||||
@@ -191,8 +191,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanReplyToRoadblock))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))]
|
||||||
private string? _latestRunSessionId;
|
private string? _latestRunSessionId;
|
||||||
|
|
||||||
|
// A resumable session to reply to a reported roadblock with (via ContinueAsync's
|
||||||
|
// transport). Distinct from ContinueCommand: that one is Failed/Cancelled-only and
|
||||||
|
// reruns with a fixed prompt, this one answers a roadblock reported on any terminal
|
||||||
|
// state (see ShowRoadblockCard) with the user's own text.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))]
|
||||||
|
private string _roadblockReplyDraft = string.Empty;
|
||||||
|
|
||||||
|
public bool CanReplyToRoadblock => !string.IsNullOrEmpty(LatestRunSessionId);
|
||||||
|
|
||||||
|
// Surfaces a UI-originated failure (e.g. override slot busy) to the shell's footer
|
||||||
|
// error strip — mirrors TasksIslandViewModel.ErrorReported.
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
[ObservableProperty] private string? _worktreePath;
|
||||||
@@ -311,6 +327,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Prep = new PrepPanelViewModel(worker);
|
Prep = new PrepPanelViewModel(worker);
|
||||||
|
|
||||||
Notes = new NotesEditorViewModel(_notesApi);
|
Notes = new NotesEditorViewModel(_notesApi);
|
||||||
|
Notes.ErrorReported += msg => { if (ShowErrorAsync is not null) _ = ShowErrorAsync(msg); };
|
||||||
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
||||||
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||||
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
|
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
|
||||||
@@ -332,6 +349,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
DequeueCommand.NotifyCanExecuteChanged();
|
DequeueCommand.NotifyCanExecuteChanged();
|
||||||
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
||||||
ContinueCommand.NotifyCanExecuteChanged();
|
ContinueCommand.NotifyCanExecuteChanged();
|
||||||
|
SendRoadblockReplyCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
||||||
@@ -362,6 +380,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
|
// The bound row's own status can change from outside (worker run, MCP tool, another
|
||||||
|
// session) — refresh it directly instead of relying on TasksIslandViewModel mutating
|
||||||
|
// the same instance, so the detail pane stays correct even if that row was replaced.
|
||||||
|
if (Task?.Id == taskId) _ = RefreshBoundTaskAsync(taskId);
|
||||||
};
|
};
|
||||||
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
||||||
|
|
||||||
@@ -519,6 +541,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Task = row;
|
Task = row;
|
||||||
OnPropertyChanged(nameof(TaskIdBadge));
|
OnPropertyChanged(nameof(TaskIdBadge));
|
||||||
Monitor.Reset();
|
Monitor.Reset();
|
||||||
|
RoadblockReplyDraft = string.Empty;
|
||||||
Subtasks.Clear();
|
Subtasks.Clear();
|
||||||
ChildOutcomes.Clear();
|
ChildOutcomes.Clear();
|
||||||
Attachments.Clear();
|
Attachments.Clear();
|
||||||
@@ -570,8 +593,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Model = entity.Model;
|
Model = entity.Model;
|
||||||
_listWorkingDir = entity.List?.WorkingDir;
|
_listWorkingDir = entity.List?.WorkingDir;
|
||||||
WorktreePath = entity.Worktree?.Path;
|
WorktreePath = entity.Worktree?.Path;
|
||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
// A worktree-less list-handler host task (Mission Control's "Let Claude handle it")
|
||||||
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
// has no WorktreeEntity, so its review range falls back to HandlerBaseCommit/
|
||||||
|
// HandlerHeadCommit -- see TaskEntity for why.
|
||||||
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
|
||||||
|
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
@@ -763,6 +789,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refreshes the bound row itself (status, planning phase, worktree/branch mirrors, etc.) from
|
||||||
|
// the DB. Kept independent of TasksIslandViewModel's own handler: the row instance backing
|
||||||
|
// Task may have been replaced by a full list reload, so this must not assume it stayed live.
|
||||||
|
private async System.Threading.Tasks.Task RefreshBoundTaskAsync(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.Include(t => t.List)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
if (entity is null || Task?.Id != taskId) return;
|
||||||
|
Task.UpdateFromEntity(entity);
|
||||||
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
|
}
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -777,8 +823,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
_listWorkingDir = entity.List?.WorkingDir;
|
_listWorkingDir = entity.List?.WorkingDir;
|
||||||
WorktreePath = entity.Worktree?.Path;
|
WorktreePath = entity.Worktree?.Path;
|
||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
// A worktree-less list-handler host task (Mission Control's "Let Claude handle it")
|
||||||
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
// has no WorktreeEntity, so its review range falls back to HandlerBaseCommit/
|
||||||
|
// HandlerHeadCommit -- see TaskEntity for why.
|
||||||
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
|
||||||
|
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
||||||
@@ -812,7 +861,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -954,24 +1002,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* offline */ }
|
catch { /* offline */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick up in a terminal only where a session + worktree reliably still exist (parked
|
|
||||||
// for review or failed mid-run); the worker reports a clear error otherwise.
|
|
||||||
public bool CanPickUpInTerminal => Task is not null
|
|
||||||
&& Task.Status is ClaudeDo.Data.Models.TaskStatus.WaitingForReview
|
|
||||||
or ClaudeDo.Data.Models.TaskStatus.Failed;
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task PickUpInTerminalAsync()
|
|
||||||
{
|
|
||||||
if (Task is null) return;
|
|
||||||
ClaudeDo.Ui.Services.ForegroundHelper.AllowAny();
|
|
||||||
try { await _worker.ResumeTaskInTerminalAsync(Task.Id); }
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanEnqueue))]
|
[RelayCommand(CanExecute = nameof(CanEnqueue))]
|
||||||
private async System.Threading.Tasks.Task EnqueueAsync()
|
private async System.Threading.Tasks.Task EnqueueAsync()
|
||||||
{
|
{
|
||||||
@@ -1013,6 +1043,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private bool CanContinue() =>
|
private bool CanContinue() =>
|
||||||
Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);
|
Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSendRoadblockReply))]
|
||||||
|
private async System.Threading.Tasks.Task SendRoadblockReplyAsync()
|
||||||
|
{
|
||||||
|
if (Task == null) return;
|
||||||
|
var text = RoadblockReplyDraft;
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _worker.ContinueTaskAsync(Task.Id, text);
|
||||||
|
RoadblockReplyDraft = string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("details.roadblockReply.failed", ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanSendRoadblockReply() =>
|
||||||
|
Task != null && _worker.IsConnected && CanReplyToRoadblock && !string.IsNullOrWhiteSpace(RoadblockReplyDraft);
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanResetAndRetry))]
|
[RelayCommand(CanExecute = nameof(CanResetAndRetry))]
|
||||||
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
private async System.Threading.Tasks.Task ResetAndRetryAsync()
|
||||||
{
|
{
|
||||||
@@ -1061,6 +1111,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
if (!hasChildren && result?.Status == "conflict")
|
if (!hasChildren && result?.Status == "conflict")
|
||||||
await _merge.ResolveConflictAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
await _merge.ResolveConflictAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
|
// The merge itself already landed; the verify command failed, so the task stayed
|
||||||
|
// out of Done. Surface that instead of silently looking like nothing happened.
|
||||||
|
else if (!hasChildren && result?.Status == "verify_failed" && ShowErrorAsync != null)
|
||||||
|
await ShowErrorAsync(result.ErrorMessage ?? Loc.T("vm.detailsIsland.verifyFailed"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public sealed partial class ListNavItemViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isManual;
|
[ObservableProperty] private bool _isManual;
|
||||||
[ObservableProperty] private bool _dropHintAbove;
|
[ObservableProperty] private bool _dropHintAbove;
|
||||||
[ObservableProperty] private bool _dropHintBelow;
|
[ObservableProperty] private bool _dropHintBelow;
|
||||||
|
// Set while a dragged task hovers over this row as a move target — distinct from
|
||||||
|
// DropHintAbove/Below, which mark an insertion point for list reordering.
|
||||||
|
[ObservableProperty] private bool _isTaskDropTarget;
|
||||||
public string? IconKey { get; init; }
|
public string? IconKey { get; init; }
|
||||||
public string? DotColorKey { get; init; }
|
public string? DotColorKey { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
public bool ShowMergePreviewMuted =>
|
public bool ShowMergePreviewMuted =>
|
||||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||||
|
|
||||||
|
// CanDiffMergedRange covers a worktree-less list-handler host task (no live worktree, but
|
||||||
|
// a HandlerBaseCommit/HandlerHeadCommit review range over the list's working dir) so its
|
||||||
|
// merge/diff card still renders even though _worktreePath stays null for it.
|
||||||
public bool ShowMergeSection =>
|
public bool ShowMergeSection =>
|
||||||
_worktreePath != null || _isPlanningParent || _hasChildOutcomes;
|
_worktreePath != null || _isPlanningParent || _hasChildOutcomes || CanDiffMergedRange;
|
||||||
|
|
||||||
public Func<DiffViewerViewModel, System.Threading.Tasks.Task>? ShowDiffViewer { get; set; }
|
public Func<DiffViewerViewModel, System.Threading.Tasks.Task>? ShowDiffViewer { get; set; }
|
||||||
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
|||||||
|
|
||||||
public NotesEditorViewModel(INotesApi api) => _api = api;
|
public NotesEditorViewModel(INotesApi api) => _api = api;
|
||||||
|
|
||||||
|
// Raised when a worker call fails so the host VM can surface it (e.g. via ShowErrorAsync).
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
||||||
@@ -55,9 +58,16 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
var text = NewBulletText.Trim();
|
var text = NewBulletText.Trim();
|
||||||
if (text.Length == 0) return;
|
if (text.Length == 0) return;
|
||||||
var dto = await _api.AddAsync(CurrentDay, text);
|
try
|
||||||
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
{
|
||||||
NewBulletText = "";
|
var dto = await _api.AddAsync(CurrentDay, text);
|
||||||
|
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||||
|
NewBulletText = "";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ using ClaudeDo.Data.Repositories;
|
|||||||
using ClaudeDo.Ui.Helpers;
|
using ClaudeDo.Ui.Helpers;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
@@ -28,13 +27,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
|||||||
|
|
||||||
[ObservableProperty] private string _agentState = "idle";
|
[ObservableProperty] private string _agentState = "idle";
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
|
|
||||||
private string? _title;
|
|
||||||
|
|
||||||
public string DisplayTitle =>
|
|
||||||
string.IsNullOrWhiteSpace(Title) ? (SubscribedTaskId ?? "task") : Title!;
|
|
||||||
|
|
||||||
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
public bool IsIdle => AgentState == "idle";
|
public bool IsIdle => AgentState == "idle";
|
||||||
public bool IsQueued => AgentState == "queued";
|
public bool IsQueued => AgentState == "queued";
|
||||||
@@ -205,43 +197,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
|
|||||||
ClearPendingQuestion();
|
ClearPendingQuestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(DetachTooltip))]
|
|
||||||
private bool _isDetached;
|
|
||||||
|
|
||||||
// Localized tooltip for the detach/re-dock toggle button.
|
|
||||||
public string DetachTooltip => Loc.T(IsDetached ? "missionControl.redock" : "missionControl.detach");
|
|
||||||
|
|
||||||
// Set by the detached window so the re-dock action can close it.
|
|
||||||
public Action? CloseWindowRequested { get; set; }
|
|
||||||
|
|
||||||
// Set by the host (e.g. Mission Control) to navigate the main app to this task.
|
|
||||||
public Action<string>? OpenInAppRequested { get; set; }
|
|
||||||
|
|
||||||
// Set by the host (Mission Control) to pop this monitor out into its own window.
|
|
||||||
public Action<TaskMonitorViewModel>? DetachRequested { get; set; }
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Detach()
|
|
||||||
{
|
|
||||||
if (IsDetached) CloseWindowRequested?.Invoke(); // re-dock: close the detached window
|
|
||||||
else DetachRequested?.Invoke(this); // detach: pop out to its own window
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenInApp()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId))
|
|
||||||
OpenInAppRequested?.Invoke(_subscribedTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task CancelTask()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && (IsRunning || IsQueued))
|
|
||||||
await _worker.CancelTaskAsync(_subscribedTaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTaskId(string id) => _subscribedTaskId = id;
|
public void SetTaskId(string id) => _subscribedTaskId = id;
|
||||||
|
|
||||||
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
|
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
|
||||||
|
|||||||
@@ -74,11 +74,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
&& !IsManual;
|
&& !IsManual;
|
||||||
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
|
||||||
|
|
||||||
// Pick up in a terminal only where a session + worktree reliably still exist: a task
|
|
||||||
// parked for review, or one that failed mid-run. The worker validates and reports a
|
|
||||||
// clear error if there's no resumable session/worktree.
|
|
||||||
public bool CanPickUpInTerminal => Status is TaskStatus.WaitingForReview or TaskStatus.Failed;
|
|
||||||
|
|
||||||
public string? PlanningBadge => PlanningPhase switch
|
public string? PlanningBadge => PlanningPhase switch
|
||||||
{
|
{
|
||||||
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
|
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
|
||||||
@@ -177,7 +172,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(ShowStatusChip));
|
OnPropertyChanged(nameof(ShowStatusChip));
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
OnPropertyChanged(nameof(IsWaitingForReview));
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
OnPropertyChanged(nameof(CanPickUpInTerminal));
|
|
||||||
OnPropertyChanged(nameof(IsParked));
|
OnPropertyChanged(nameof(IsParked));
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
OnPropertyChanged(nameof(IsWaiting));
|
OnPropertyChanged(nameof(IsWaiting));
|
||||||
|
|||||||
@@ -231,6 +231,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_loadCts = new CancellationTokenSource();
|
_loadCts = new CancellationTokenSource();
|
||||||
var ct = _loadCts.Token;
|
var ct = _loadCts.Token;
|
||||||
|
|
||||||
|
// Items is rebuilt from scratch below, so a selection carried over from the previous list
|
||||||
|
// would leave the detail pane bound to a task the visible list no longer contains. Only a
|
||||||
|
// *different* list drops it — a reload of the same list (worker refresh, reconnect) keeps
|
||||||
|
// the selection so a live update never yanks the detail pane away.
|
||||||
|
var listChanged = !string.Equals(_currentList?.Id, list?.Id, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// A same-list reload (triggered by OnWorkerTaskUpdated's full-reload branches) rebuilds
|
||||||
|
// every row from scratch. Reusing the previous instances by id — instead of handing back
|
||||||
|
// brand new ones — keeps SelectedTask (and the bound DetailsIslandViewModel.Task) pointed
|
||||||
|
// at a live row instead of an orphan that never receives another update.
|
||||||
|
var reusable = listChanged ? null : Items.ToDictionary(r => r.Id);
|
||||||
|
|
||||||
if (_currentList is not null)
|
if (_currentList is not null)
|
||||||
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
||||||
_currentList = list;
|
_currentList = list;
|
||||||
@@ -246,6 +258,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
HasCompleted = false;
|
HasCompleted = false;
|
||||||
ShowOpenLabel = false;
|
ShowOpenLabel = false;
|
||||||
ShowNotesRow = false;
|
ShowNotesRow = false;
|
||||||
|
if (listChanged) SelectedTask = null;
|
||||||
if (list is null) { IsLetClaudeVisible = false; LoadTask = Task.CompletedTask; return; }
|
if (list is null) { IsLetClaudeVisible = false; LoadTask = Task.CompletedTask; return; }
|
||||||
|
|
||||||
HeaderTitle = list.Name;
|
HeaderTitle = list.Name;
|
||||||
@@ -254,10 +267,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
IsMyDayList = list.Id == "smart:my-day";
|
IsMyDayList = list.Id == "smart:my-day";
|
||||||
IsLetClaudeVisible = list.Kind == ListKind.User && !string.IsNullOrWhiteSpace(list.WorkingDir);
|
IsLetClaudeVisible = list.Kind == ListKind.User && !string.IsNullOrWhiteSpace(list.WorkingDir);
|
||||||
|
|
||||||
LoadTask = LoadForListAsync(list, ct);
|
LoadTask = LoadForListAsync(list, ct, reusable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadForListAsync(ListNavItemViewModel list, CancellationToken ct)
|
private async Task LoadForListAsync(
|
||||||
|
ListNavItemViewModel list, CancellationToken ct, Dictionary<string, TaskRowViewModel>? reusable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -285,7 +299,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var showListChip = list.Kind == ListKind.Virtual;
|
var showListChip = list.Kind == ListKind.Virtual;
|
||||||
foreach (var t in filteredList)
|
foreach (var t in filteredList)
|
||||||
{
|
{
|
||||||
var row = TaskRowViewModel.FromEntity(t);
|
TaskRowViewModel row;
|
||||||
|
if (reusable is not null && reusable.TryGetValue(t.Id, out var existing))
|
||||||
|
{
|
||||||
|
row = existing;
|
||||||
|
row.UpdateFromEntity(t);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row = TaskRowViewModel.FromEntity(t);
|
||||||
|
}
|
||||||
row.ShowListChip = showListChip;
|
row.ShowListChip = showListChip;
|
||||||
row.HasInteractiveSession = _interactiveSessionIds.Contains(row.Id);
|
row.HasInteractiveSession = _interactiveSessionIds.Contains(row.Id);
|
||||||
Items.Add(row);
|
Items.Add(row);
|
||||||
@@ -471,6 +494,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
||||||
|
public string? CurrentListId => _currentList?.Id;
|
||||||
|
|
||||||
|
/// <summary>Set by the shell (mirrors <see cref="ListsIslandViewModel.Dialogs"/>) so a
|
||||||
|
/// cross-repo move can confirm via the shared modal seam.</summary>
|
||||||
|
public IDialogService? Dialogs { get; set; }
|
||||||
|
|
||||||
public void ClearDropHints()
|
public void ClearDropHints()
|
||||||
{
|
{
|
||||||
@@ -573,6 +601,76 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drag-drop move: reassigns a task (and every descendant) to <paramref name="targetList"/>.
|
||||||
|
/// No-op for anything that isn't a user list the task doesn't already sit in.
|
||||||
|
/// Rejects running tasks and tasks (or descendants) holding an Active/Kept worktree — the
|
||||||
|
/// worktree would keep pointing at the source repo. A move across repos without a worktree
|
||||||
|
/// is allowed but asks for confirmation first.
|
||||||
|
/// </summary>
|
||||||
|
public async Task MoveTaskToListAsync(TaskRowViewModel row, ListNavItemViewModel targetList)
|
||||||
|
{
|
||||||
|
if (targetList.Kind != ListKind.User) return;
|
||||||
|
var targetListId = targetList.Id.StartsWith("user:", StringComparison.Ordinal)
|
||||||
|
? targetList.Id["user:".Length..]
|
||||||
|
: targetList.Id;
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
// The row's own list, not _currentList — the task island also shows smart/virtual lists,
|
||||||
|
// whose rows come from many lists and which carry no working dir of their own. Dropping a
|
||||||
|
// task on the list it already sits in is a silent no-op.
|
||||||
|
var sourceListId = await db.Tasks.AsNoTracking()
|
||||||
|
.Where(t => t.Id == row.Id).Select(t => t.ListId).FirstOrDefaultAsync();
|
||||||
|
if (sourceListId is null || sourceListId == targetListId) return;
|
||||||
|
|
||||||
|
if (row.IsRunning)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveRunningRejected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo = new TaskRepository(db);
|
||||||
|
|
||||||
|
var descendantIds = await repo.GetDescendantIdsAsync(row.Id);
|
||||||
|
var ownIds = new List<string> { row.Id };
|
||||||
|
ownIds.AddRange(descendantIds);
|
||||||
|
var hasBlockingWorktree = await db.Worktrees.AsNoTracking()
|
||||||
|
.Where(w => ownIds.Contains(w.TaskId)
|
||||||
|
&& (w.State == ClaudeDo.Data.Models.WorktreeState.Active
|
||||||
|
|| w.State == ClaudeDo.Data.Models.WorktreeState.Kept))
|
||||||
|
.AnyAsync();
|
||||||
|
if (hasBlockingWorktree)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveWorktreeRejected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceDir = await db.Lists.AsNoTracking()
|
||||||
|
.Where(l => l.Id == sourceListId).Select(l => l.WorkingDir).FirstOrDefaultAsync();
|
||||||
|
var targetDir = targetList.WorkingDir;
|
||||||
|
var repoChanges = !string.Equals(sourceDir, targetDir, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (repoChanges)
|
||||||
|
{
|
||||||
|
if (Dialogs is null)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.moveConfirmUnavailable"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ok = await Dialogs.ConfirmAsync(Loc.T("vm.tasksIsland.moveRepoConfirm",
|
||||||
|
string.IsNullOrWhiteSpace(sourceDir) ? "—" : sourceDir,
|
||||||
|
string.IsNullOrWhiteSpace(targetDir) ? "—" : targetDir));
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await repo.MoveToListAsync(row.Id, targetListId);
|
||||||
|
|
||||||
|
Items.Remove(row);
|
||||||
|
Regroup();
|
||||||
|
UpdateSubtitle();
|
||||||
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ToggleDoneAsync(TaskRowViewModel row)
|
private async Task ToggleDoneAsync(TaskRowViewModel row)
|
||||||
{
|
{
|
||||||
@@ -884,15 +982,6 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
OpenConPtySessionRequested?.Invoke(row.Id);
|
OpenConPtySessionRequested?.Invoke(row.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task PickUpInTerminalAsync(TaskRowViewModel? row)
|
|
||||||
{
|
|
||||||
if (row is null || _worker is null) return;
|
|
||||||
ForegroundHelper.AllowAny();
|
|
||||||
try { await _worker.ResumeTaskInTerminalAsync(row.Id); }
|
|
||||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.pickUpInTerminalFailed", ex.Message)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
public DetailsIslandViewModel? Details { get; }
|
public DetailsIslandViewModel? Details { get; }
|
||||||
public IWorkerClient? Worker { get; }
|
public IWorkerClient? Worker { get; }
|
||||||
public MissionControlViewModel? MissionControl { get; }
|
public MissionControlViewModel? MissionControl { get; }
|
||||||
|
public UsagePillViewModel? UsagePill { get; }
|
||||||
public UpdateCheckService UpdateCheck => _updateCheck;
|
public UpdateCheckService UpdateCheck => _updateCheck;
|
||||||
|
|
||||||
public string ConnectionText =>
|
public string ConnectionText =>
|
||||||
@@ -36,6 +37,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
||||||
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
||||||
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
||||||
|
private readonly Func<UsageMonitorModalViewModel> _usageMonitorVmFactory = () => null!;
|
||||||
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
|
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
|
||||||
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
|
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
_dialogs = value;
|
_dialogs = value;
|
||||||
if (Lists is not null) Lists.Dialogs = value;
|
if (Lists is not null) Lists.Dialogs = value;
|
||||||
|
if (Tasks is not null) Tasks.Dialogs = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,14 +208,17 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
|
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
|
||||||
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
|
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
|
||||||
|
Func<UsageMonitorModalViewModel> usageMonitorVmFactory,
|
||||||
Func<MergeModalViewModel> mergeVmFactory,
|
Func<MergeModalViewModel> mergeVmFactory,
|
||||||
Func<RepoImportModalViewModel> repoImportVmFactory,
|
Func<RepoImportModalViewModel> repoImportVmFactory,
|
||||||
MissionControlViewModel missionControl)
|
MissionControlViewModel missionControl,
|
||||||
|
UsagePillViewModel usagePill)
|
||||||
{
|
{
|
||||||
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||||
MissionControl = missionControl;
|
MissionControl = missionControl;
|
||||||
|
UsagePill = usagePill;
|
||||||
|
UsagePill.OpenMonitorRequested += () => _ = OpenUsageMonitor();
|
||||||
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
||||||
MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
|
|
||||||
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
||||||
MissionControl.ErrorReported += FlashFooterError;
|
MissionControl.ErrorReported += FlashFooterError;
|
||||||
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
|
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
|
||||||
@@ -223,6 +229,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
|
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
|
||||||
_weeklyReportVmFactory = weeklyReportVmFactory;
|
_weeklyReportVmFactory = weeklyReportVmFactory;
|
||||||
|
_usageMonitorVmFactory = usageMonitorVmFactory;
|
||||||
_mergeVmFactory = mergeVmFactory;
|
_mergeVmFactory = mergeVmFactory;
|
||||||
_repoImportVmFactory = repoImportVmFactory;
|
_repoImportVmFactory = repoImportVmFactory;
|
||||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||||
@@ -256,6 +263,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
if (Lists.SelectedList is { } row)
|
if (Lists.SelectedList is { } row)
|
||||||
Lists.LetClaudeHandleListCommand.Execute(row);
|
Lists.LetClaudeHandleListCommand.Execute(row);
|
||||||
};
|
};
|
||||||
|
Details.ErrorReported += FlashFooterError;
|
||||||
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
||||||
Details.DeleteFromList = row =>
|
Details.DeleteFromList = row =>
|
||||||
{
|
{
|
||||||
@@ -425,6 +433,23 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
finally { _weeklyReportOpen = false; }
|
finally { _weeklyReportOpen = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _usageMonitorOpen;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenUsageMonitor()
|
||||||
|
{
|
||||||
|
if (Dialogs is null || _usageMonitorOpen) return;
|
||||||
|
_usageMonitorOpen = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vm = _usageMonitorVmFactory();
|
||||||
|
vm.ErrorReported += FlashFooterError;
|
||||||
|
await vm.LoadAsync();
|
||||||
|
await Dialogs.ShowUsageMonitorAsync(vm);
|
||||||
|
}
|
||||||
|
finally { _usageMonitorOpen = false; }
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task CheckForUpdatesAsync()
|
private async Task CheckForUpdatesAsync()
|
||||||
{
|
{
|
||||||
@@ -495,7 +520,13 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
// WorkingDirectory must NOT stay empty: the child would inherit ours (<InstallDir>\app)
|
||||||
|
// and its locked current directory blocks the installer's own app\ rename.
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = System.IO.Path.GetTempPath(),
|
||||||
|
});
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
|
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
|
||||||
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, <see cref="TaskId"/>
|
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, <see cref="TaskId"/>
|
||||||
/// is null — ad-hoc panes are never deduped, unlike task-based ones). Distinct from the streamed-log
|
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
|
||||||
/// <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> pane; the two coexist until
|
|
||||||
/// the streaming interactive stack is removed.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common contract for anything hosted as a pane in the Command Center — implemented by both
|
/// Common contract for anything hosted as a pane in the Command Center — currently only
|
||||||
/// the streamed-log <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> and the
|
/// <see cref="ConPtyPaneViewModel"/>, kept as its own abstraction so the grid/tabs layout
|
||||||
/// embedded ConPTY <see cref="ConPtyPaneViewModel"/> — so the layout toggle (grid/tabs) can
|
/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
|
||||||
/// bind one heterogeneous pane collection.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMissionControlPane
|
public interface IMissionControlPane
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ using System.Linq;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -23,14 +21,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Action<string> _onTaskUpdated;
|
private readonly Action<string> _onTaskUpdated;
|
||||||
private readonly Action _onConnectionRestored;
|
private readonly Action _onConnectionRestored;
|
||||||
|
|
||||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
|
||||||
|
|
||||||
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
||||||
// review/merge/status machinery. Mirrored into Panes alongside the streamed-log Monitors.
|
// review/merge/status machinery.
|
||||||
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
|
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
|
||||||
|
|
||||||
// Unified view of Monitors ++ ConPtySessions (in that order) so the layout toggle can
|
// Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs)
|
||||||
// present one heterogeneous collection as either a grid or tabs.
|
// binds one contract rather than a ConPTY-specific type.
|
||||||
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private int _columnCount = 1;
|
[ObservableProperty] private int _columnCount = 1;
|
||||||
@@ -47,41 +43,29 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
|
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
|
||||||
public event Action<string>? ErrorReported;
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
private Action<string>? _openInApp;
|
public Action<string>? OpenInApp { get; set; }
|
||||||
public Action<string>? OpenInApp
|
|
||||||
{
|
|
||||||
get => _openInApp;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_openInApp = value;
|
|
||||||
foreach (var m in Monitors) m.OpenInAppRequested = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View-layer seam: show a detached monitor in its own window. Second arg is the re-dock callback
|
|
||||||
// invoked when that window closes.
|
|
||||||
public Action<TaskMonitorViewModel, Action>? ShowDetached { get; set; }
|
|
||||||
|
|
||||||
// View-layer seam: open the app Settings modal from the Mission Control window.
|
// View-layer seam: open the app Settings modal from the Mission Control window.
|
||||||
public Action? OpenSettingsRequested { get; set; }
|
public Action? OpenSettingsRequested { get; set; }
|
||||||
|
|
||||||
public bool HasMonitors => Monitors.Count > 0;
|
|
||||||
public bool HasPanes => Panes.Count > 0;
|
public bool HasPanes => Panes.Count > 0;
|
||||||
|
|
||||||
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
||||||
public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
|
public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
|
||||||
public bool HasQueued => Queued.Count > 0;
|
public bool HasQueued => Queued.Count > 0;
|
||||||
|
|
||||||
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
|
public UsagePillViewModel UsagePill { get; }
|
||||||
|
|
||||||
|
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, UsagePillViewModel usagePill)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
UsagePill = usagePill;
|
||||||
|
|
||||||
Monitors.CollectionChanged += OnMonitorsChanged;
|
|
||||||
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
||||||
Panes.CollectionChanged += OnPanesChanged;
|
Panes.CollectionChanged += OnPanesChanged;
|
||||||
|
|
||||||
_onTaskStarted = (slot, taskId, startedAt) => { EnsureMonitor(taskId); _ = RefreshQueueAsync(); };
|
_onTaskStarted = (slot, taskId, startedAt) => { _ = RefreshQueueAsync(); };
|
||||||
_worker.TaskStartedEvent += _onTaskStarted;
|
_worker.TaskStartedEvent += _onTaskStarted;
|
||||||
|
|
||||||
_onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
|
_onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
|
||||||
@@ -90,10 +74,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_onTaskUpdated = taskId => _ = RefreshQueueAsync();
|
_onTaskUpdated = taskId => _ = RefreshQueueAsync();
|
||||||
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
||||||
|
|
||||||
_onConnectionRestored = () => { SeedActive(); _ = RefreshQueueAsync(); };
|
_onConnectionRestored = () => { _ = RefreshQueueAsync(); };
|
||||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||||
|
|
||||||
SeedActive();
|
|
||||||
_ = RefreshQueueAsync();
|
_ = RefreshQueueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +86,26 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var rows = await ctx.Tasks.AsNoTracking()
|
var rows = await ctx.Tasks.AsNoTracking()
|
||||||
.Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued)
|
.Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued
|
||||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
|| t.Status == ClaudeDo.Data.Models.TaskStatus.Running)
|
||||||
.Select(t => new { t.Id, t.Title, t.BlockedByTaskId })
|
.OrderBy(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Running ? 0 : 1)
|
||||||
|
.ThenBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.Select(t => new { t.Id, t.Title, t.BlockedByTaskId, t.Status })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
Queued.Clear();
|
Queued.Clear();
|
||||||
foreach (var r in rows)
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
var id = r.Id;
|
||||||
Queued.Add(new QueuedTaskViewModel
|
Queued.Add(new QueuedTaskViewModel
|
||||||
{
|
{
|
||||||
Id = r.Id,
|
Id = id,
|
||||||
Title = r.Title ?? string.Empty,
|
Title = r.Title ?? string.Empty,
|
||||||
IsBlocked = r.BlockedByTaskId != null,
|
IsBlocked = r.BlockedByTaskId != null,
|
||||||
|
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
|
||||||
|
OpenInAppCommand = new RelayCommand(() => OpenInApp?.Invoke(id)),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
OnPropertyChanged(nameof(HasQueued));
|
OnPropertyChanged(nameof(HasQueued));
|
||||||
}
|
}
|
||||||
catch { /* best-effort queue refresh */ }
|
catch { /* best-effort queue refresh */ }
|
||||||
@@ -141,86 +131,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
await RefreshQueueAsync();
|
await RefreshQueueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SeedActive()
|
|
||||||
{
|
|
||||||
foreach (var a in _worker.GetActiveTasks())
|
|
||||||
EnsureMonitor(a.TaskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureMonitor(string taskId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(taskId)) return;
|
|
||||||
if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;
|
|
||||||
|
|
||||||
var monitor = new TaskMonitorViewModel(_dbFactory, _worker);
|
|
||||||
monitor.SetTaskId(taskId);
|
|
||||||
monitor.OpenInAppRequested = _openInApp;
|
|
||||||
monitor.DetachRequested = Detach;
|
|
||||||
Monitors.Add(monitor);
|
|
||||||
_ = HydrateAsync(monitor, taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Detach(TaskMonitorViewModel monitor)
|
|
||||||
{
|
|
||||||
if (!Monitors.Contains(monitor)) return;
|
|
||||||
monitor.IsDetached = true;
|
|
||||||
Monitors.Remove(monitor); // drop from grid — do NOT dispose; it keeps streaming
|
|
||||||
ShowDetached?.Invoke(monitor, () => ReDock(monitor));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReDock(TaskMonitorViewModel monitor)
|
|
||||||
{
|
|
||||||
monitor.IsDetached = false;
|
|
||||||
if (!Monitors.Contains(monitor) && monitor.SubscribedTaskId is not null)
|
|
||||||
Monitors.Add(monitor); // back into the grid
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task HydrateAsync(TaskMonitorViewModel monitor, string taskId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
||||||
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
|
||||||
if (entity is null || monitor.SubscribedTaskId != taskId) return;
|
|
||||||
monitor.ApplyState(entity.Status);
|
|
||||||
monitor.Title = entity.Title;
|
|
||||||
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
|
||||||
monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
|
||||||
await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);
|
|
||||||
|
|
||||||
// Re-attach: if the task is blocked on an AskUser question right now, surface it.
|
|
||||||
var pending = await _worker.GetPendingQuestionAsync(taskId);
|
|
||||||
if (pending is not null && monitor.SubscribedTaskId == taskId)
|
|
||||||
monitor.SetPendingQuestion(pending.QuestionId, pending.Question);
|
|
||||||
}
|
|
||||||
catch { /* best-effort hydrate */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void ClearFinished()
|
|
||||||
{
|
|
||||||
foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled || m.IsWaitingForReview).ToList())
|
|
||||||
{
|
|
||||||
Monitors.Remove(m);
|
|
||||||
m.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
|
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
|
||||||
|
|
||||||
public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(dragged, target)) return;
|
|
||||||
var from = Monitors.IndexOf(dragged);
|
|
||||||
var to = Monitors.IndexOf(target);
|
|
||||||
if (from < 0 || to < 0) return;
|
|
||||||
Monitors.Move(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
||||||
// Command Center pane (task-based only).
|
// Command Center pane (task-based only).
|
||||||
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||||
@@ -286,22 +202,44 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
return System.Threading.Tasks.Task.CompletedTask;
|
return System.Threading.Tasks.Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List-handler session over a hand-picked set of tasks ("Let Claude handle it").
|
// List-handler session over a hand-picked set of tasks ("Let Claude handle it"). Task-based:
|
||||||
// Ad-hoc style: no owning task, never deduped — every run opens a fresh pane.
|
// creates one new ClaudeDo task per run to own the session (title/diff/result), deduped by
|
||||||
|
// TaskId like OpenConPtySessionAsync. The handler still merges the tasks it handles itself
|
||||||
|
// (no worktree of its own) — see TaskEntity.HandlerBaseCommit/HandlerHeadCommit.
|
||||||
public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
||||||
{
|
{
|
||||||
if (taskIds is not { Count: > 0 }) return;
|
if (taskIds is not { Count: > 0 }) return;
|
||||||
|
|
||||||
var title = Loc.T("missionControl.mergeHelperTitle");
|
var title = Loc.T("missionControl.mergeHelperTitle");
|
||||||
|
var listName = listId;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId);
|
var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId);
|
||||||
if (list?.Name is { Length: > 0 } name) title = $"{title} — {name}";
|
if (list?.Name is { Length: > 0 } name) { listName = name; title = $"{title} — {name}"; }
|
||||||
}
|
}
|
||||||
catch { /* best-effort title lookup */ }
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title,
|
string taskId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
taskId = await _worker.CreateMergeHelperTaskAsync(taskIds, listId,
|
||||||
|
Loc.T("missionControl.mergeHelperTaskTitle", listName),
|
||||||
|
Loc.T("missionControl.mergeHelperTaskDescriptionHeader"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||||
|
{
|
||||||
|
FocusedPane = existing;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId))));
|
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,42 +289,19 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
pane.Dispose();
|
pane.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
|
// Mirrors ConPtySessions' add/remove 1:1 into Panes (typed as the pane abstraction).
|
||||||
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
switch (e.Action)
|
|
||||||
{
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
|
||||||
Panes.Insert(e.NewStartingIndex, (TaskMonitorViewModel)e.NewItems![0]!);
|
|
||||||
break;
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
|
||||||
Panes.RemoveAt(e.OldStartingIndex);
|
|
||||||
break;
|
|
||||||
case NotifyCollectionChangedAction.Move:
|
|
||||||
Panes.Move(e.OldStartingIndex, e.NewStartingIndex);
|
|
||||||
break;
|
|
||||||
default: // Reset (Dispose's Monitors.Clear())
|
|
||||||
foreach (var p in Panes.OfType<TaskMonitorViewModel>().ToList())
|
|
||||||
Panes.Remove(p);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
OnPropertyChanged(nameof(HasMonitors));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors ConPtySessions' add/remove into the tail (ConPtySessions-suffix) section of Panes.
|
|
||||||
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
Panes.Insert(e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
||||||
break;
|
break;
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
|
Panes.RemoveAt(e.OldStartingIndex);
|
||||||
break;
|
break;
|
||||||
default: // Reset
|
default: // Reset
|
||||||
foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
|
Panes.Clear();
|
||||||
Panes.Remove(p);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,11 +327,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.TaskFinishedEvent -= _onTaskFinished;
|
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
|
||||||
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||||
Panes.CollectionChanged -= OnPanesChanged;
|
Panes.CollectionChanged -= OnPanesChanged;
|
||||||
foreach (var m in Monitors) m.Dispose();
|
|
||||||
Monitors.Clear();
|
|
||||||
foreach (var c in ConPtySessions.ToList())
|
foreach (var c in ConPtySessions.ToList())
|
||||||
{
|
{
|
||||||
c.ErrorReported -= OnConPtyPaneError;
|
c.ErrorReported -= OnConPtyPaneError;
|
||||||
@@ -428,10 +340,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Read-only display row for a queued task in the Mission Control side strip.</summary>
|
/// <summary>Read-only display row for a queued or running task in the Mission Control side strip.</summary>
|
||||||
public sealed class QueuedTaskViewModel
|
public sealed class QueuedTaskViewModel
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string Title { get; init; }
|
public required string Title { get; init; }
|
||||||
public bool IsBlocked { get; init; }
|
public bool IsBlocked { get; init; }
|
||||||
|
public bool IsRunning { get; init; }
|
||||||
|
public IRelayCommand? OpenInAppCommand { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
|||||||
DisplayedDiff = "";
|
DisplayedDiff = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DisplayedDiff = "";
|
||||||
|
CombinedWarning = Loc.T("vm.planningDiff.buildFailed", ex.Message);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsLoadingCombined = false;
|
IsLoadingCombined = false;
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
||||||
// A manual list holds reminders: tasks created here start out manual (TaskEntity.IsManual).
|
// A manual list holds reminders: tasks created here start out manual (TaskEntity.IsManual).
|
||||||
[ObservableProperty] private bool _isManual;
|
[ObservableProperty] private bool _isManual;
|
||||||
|
// Optional post-merge verification command (build/test), run in WorkingDir after a merge
|
||||||
|
// lands; a non-zero exit keeps the task out of Done instead of silently reporting merged.
|
||||||
|
[ObservableProperty] private string _verifyCommand = "";
|
||||||
|
|
||||||
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
||||||
|
|
||||||
@@ -61,6 +64,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
|
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
|
||||||
|
|
||||||
await Agent.LoadForListAsync(listId, ct);
|
await Agent.LoadForListAsync(listId, ct);
|
||||||
|
var cfg = await _worker.GetListConfigAsync(listId);
|
||||||
|
VerifyCommand = cfg?.VerifyCommand ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -73,7 +78,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
DefaultCommitType,
|
DefaultCommitType,
|
||||||
IsManual));
|
IsManual));
|
||||||
|
|
||||||
await Agent.SaveAsync();
|
await Agent.SaveAsync(string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand);
|
||||||
|
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
|||||||
IEnumerable<WorkerLogEntry> items = WarnErrorOnly
|
IEnumerable<WorkerLogEntry> items = WarnErrorOnly
|
||||||
? _all.Where(e => e.Level is WorkerLogLevel.Warn or WorkerLogLevel.Error)
|
? _all.Where(e => e.Level is WorkerLogLevel.Warn or WorkerLogLevel.Error)
|
||||||
: _all;
|
: _all;
|
||||||
foreach (var e in items)
|
foreach (var e in items.Reverse())
|
||||||
Rows.Add(new LogVisualizerRow(e.TimestampUtc.ToLocalTime().ToString("HH:mm:ss"), e.Message, e.Level));
|
Rows.Add(new LogVisualizerRow(e.TimestampUtc.ToLocalTime().ToString("HH:mm:ss"), e.Message, e.Level));
|
||||||
StatusText = Rows.Count == 0
|
StatusText = Rows.Count == 0
|
||||||
? Loc.T("modals.logVisualizer.empty")
|
? Loc.T("modals.logVisualizer.empty")
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||||
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
||||||
[ObservableProperty] private int _maxParallelExecutions = 1;
|
[ObservableProperty] private int _maxParallelExecutions = 1;
|
||||||
|
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||||
|
[ObservableProperty] private int _usageGateFiveHourPct = 80;
|
||||||
|
[ObservableProperty] private int _usageGateSevenDayPct = 90;
|
||||||
// Newline-separated path prefixes excluded from the weekly report.
|
// Newline-separated path prefixes excluded from the weekly report.
|
||||||
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
||||||
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
||||||
@@ -91,6 +94,10 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
if (MaxParallelExecutions < 1 || MaxParallelExecutions > 20)
|
||||||
return "Max parallel executions must be between 1 and 20.";
|
return "Max parallel executions must be between 1 and 20.";
|
||||||
|
if (UsageGateFiveHourPct < 0 || UsageGateFiveHourPct > 100)
|
||||||
|
return "Usage gate (5h) must be between 0 and 100.";
|
||||||
|
if (UsageGateSevenDayPct < 0 || UsageGateSevenDayPct > 100)
|
||||||
|
return "Usage gate (7d) must be between 0 and 100.";
|
||||||
foreach (var row in ModelPresets)
|
foreach (var row in ModelPresets)
|
||||||
if (row.MaxTurns is < 1 or > 200)
|
if (row.MaxTurns is < 1 or > 200)
|
||||||
return $"Max turns for {row.Model} must be between 1 and 200.";
|
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
|
|||||||
var r = await _worker.CleanupFinishedWorktreesAsync();
|
var r = await _worker.CleanupFinishedWorktreesAsync();
|
||||||
StatusMessage = r is null ? Loc.T("vm.worktreesTab.workerOffline") : Loc.T("vm.worktreesTab.removed", r.Removed);
|
StatusMessage = r is null ? Loc.T("vm.worktreesTab.workerOffline") : Loc.T("vm.worktreesTab.removed", r.Removed);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesTab.cleanupFailed", ex.Message); }
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
|
|||||||
else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks);
|
else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks);
|
||||||
else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected);
|
else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesTab.resetFailed", ex.Message); }
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||||
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||||
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
||||||
|
General.UsageGateFiveHourPct = dto.UsageGateFiveHourPct;
|
||||||
|
General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct;
|
||||||
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
Worktrees.WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||||
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||||
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||||
@@ -109,7 +111,9 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.StandupWeekday,
|
General.StandupWeekday,
|
||||||
Prime.DailyPrepMaxTasks,
|
Prime.DailyPrepMaxTasks,
|
||||||
General.SelectedSessionSkillNames(),
|
General.SelectedSessionSkillNames(),
|
||||||
General.ModelPresetDtos());
|
General.ModelPresetDtos(),
|
||||||
|
General.UsageGateFiveHourPct,
|
||||||
|
General.UsageGateSevenDayPct);
|
||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
await OnlineInbox.SaveAsync();
|
await OnlineInbox.SaveAsync();
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
private bool _applyingRange;
|
||||||
|
|
||||||
|
public UsageMonitorModalViewModel(IWorkerClient worker) => _worker = worker;
|
||||||
|
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(GaugeRows))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsStale))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(LastError))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsGateBlocked))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(GateReason))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(StaleStampText))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(StaleBandText))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(GateBandText))]
|
||||||
|
private UsageSnapshotDto? _snapshot;
|
||||||
|
|
||||||
|
[ObservableProperty] private DateTime? _startDate;
|
||||||
|
[ObservableProperty] private DateTime? _endDate;
|
||||||
|
[ObservableProperty] private int _selectedPresetDays = 7;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
||||||
|
private bool _isBusy;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
||||||
|
private IReadOnlyList<ModelUsageDisplayRow> _modelRows = Array.Empty<ModelUsageDisplayRow>();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TasksEmpty))]
|
||||||
|
private IReadOnlyList<TaskUsageDisplayRow> _taskRows = Array.Empty<TaskUsageDisplayRow>();
|
||||||
|
|
||||||
|
public bool ModelsEmpty => !IsBusy && ModelRows.Count == 0;
|
||||||
|
public bool TasksEmpty => !IsBusy && TaskRows.Count == 0;
|
||||||
|
|
||||||
|
public IReadOnlyList<UsageGaugeRowViewModel> GaugeRows =>
|
||||||
|
Snapshot is null ? Array.Empty<UsageGaugeRowViewModel>() : Snapshot.Limits.Select(BuildGaugeRow).ToList();
|
||||||
|
|
||||||
|
public bool IsStale => Snapshot?.IsStale == true;
|
||||||
|
public string? LastError => Snapshot?.LastError;
|
||||||
|
public bool IsGateBlocked => Snapshot?.IsGateBlocked == true;
|
||||||
|
public string? GateReason => Snapshot?.GateReason;
|
||||||
|
public string StaleStampText => Snapshot?.FetchedAtUtc is { } t ? t.ToLocalTime().ToString("HH:mm") : "?";
|
||||||
|
public string StaleBandText => Loc.T("modals.usageMonitor.staleFormat", StaleStampText);
|
||||||
|
public string GateBandText => GateReason is null ? "" : Loc.T("modals.usageMonitor.gateBlockedFormat", GateReason);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
Snapshot = await _worker.GetUsageSnapshotAsync();
|
||||||
|
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
||||||
|
_worker.UsageUpdatedEvent += OnUsageUpdated;
|
||||||
|
ApplyPresetRange(SelectedPresetDays);
|
||||||
|
await LoadUsageDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUsageUpdated(UsageSnapshotDto snapshot) => Snapshot = snapshot;
|
||||||
|
|
||||||
|
private void ApplyPresetRange(int days)
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
_applyingRange = true;
|
||||||
|
SelectedPresetDays = days;
|
||||||
|
StartDate = today.AddDays(-(days - 1)).ToDateTime(TimeOnly.MinValue);
|
||||||
|
EndDate = today.ToDateTime(TimeOnly.MinValue);
|
||||||
|
_applyingRange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task SetPreset7Days() => SetPreset(7);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task SetPreset30Days() => SetPreset(30);
|
||||||
|
|
||||||
|
private Task SetPreset(int days)
|
||||||
|
{
|
||||||
|
ApplyPresetRange(days);
|
||||||
|
return LoadUsageDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnStartDateChanged(DateTime? value)
|
||||||
|
{
|
||||||
|
if (!_applyingRange) _ = LoadUsageDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnEndDateChanged(DateTime? value)
|
||||||
|
{
|
||||||
|
if (!_applyingRange) _ = LoadUsageDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool RangeValid => StartDate is not null && EndDate is not null && StartDate <= EndDate;
|
||||||
|
|
||||||
|
private async Task LoadUsageDataAsync()
|
||||||
|
{
|
||||||
|
if (!RangeValid) return;
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var from = DateOnly.FromDateTime(StartDate!.Value);
|
||||||
|
var to = DateOnly.FromDateTime(EndDate!.Value);
|
||||||
|
var modelRows = await _worker.GetModelUsageAsync(from, to);
|
||||||
|
var taskRows = await _worker.GetTaskUsageAsync(from, to);
|
||||||
|
ModelRows = BuildModelDisplayRows(modelRows);
|
||||||
|
TaskRows = taskRows
|
||||||
|
.Select(r => new TaskUsageDisplayRow(r.TaskId, r.TaskTitle, r.ListName, r.Model, r.Runs, r.TokensIn, r.TokensOut))
|
||||||
|
.OrderByDescending(r => r.TotalTokens)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.loadFailed", ex.Message));
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildGaugeLabel(UsageLimitDto limit) => limit.Kind switch
|
||||||
|
{
|
||||||
|
"session" => Loc.T("modals.usageMonitor.gaugeSession"),
|
||||||
|
"weekly_all" => Loc.T("modals.usageMonitor.gaugeWeeklyAll"),
|
||||||
|
"weekly_scoped" when !string.IsNullOrWhiteSpace(limit.ScopeModelDisplayName)
|
||||||
|
=> Loc.T("modals.usageMonitor.gaugeWeeklyScopedFormat", limit.ScopeModelDisplayName!),
|
||||||
|
_ => limit.Kind,
|
||||||
|
};
|
||||||
|
|
||||||
|
private UsageGaugeRowViewModel BuildGaugeRow(UsageLimitDto limit)
|
||||||
|
{
|
||||||
|
int? threshold = limit.Kind switch
|
||||||
|
{
|
||||||
|
"session" => Snapshot?.FiveHourThresholdPct,
|
||||||
|
"weekly_all" => Snapshot?.SevenDayThresholdPct,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
return new UsageGaugeRowViewModel(BuildGaugeLabel(limit), limit.Percent, limit.Severity, limit.ResetsAt, threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<ModelUsageDisplayRow> BuildModelDisplayRows(IReadOnlyList<ModelUsageRowDto> rows)
|
||||||
|
{
|
||||||
|
var built = new List<ModelUsageDisplayRow>();
|
||||||
|
foreach (var group in rows.GroupBy(r => r.Model))
|
||||||
|
{
|
||||||
|
long cdIn = 0, cdOut = 0, cdCache = 0, otIn = 0, otOut = 0, otCache = 0;
|
||||||
|
foreach (var row in group)
|
||||||
|
{
|
||||||
|
var cache = row.CacheReadTokens + row.CacheCreationTokens;
|
||||||
|
if (string.Equals(row.Scope, "ClaudeDo", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
cdIn += row.InputTokens; cdOut += row.OutputTokens; cdCache += cache;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
otIn += row.InputTokens; otOut += row.OutputTokens; otCache += cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
built.Add(new ModelUsageDisplayRow(group.Key, cdIn, cdOut, cdCache, otIn, otOut, otCache));
|
||||||
|
}
|
||||||
|
|
||||||
|
var grandTotal = built.Sum(r => r.GrandTotal);
|
||||||
|
return built
|
||||||
|
.Select(r => r with { SharePercent = grandTotal > 0 ? r.GrandTotal * 100.0 / grandTotal : 0 })
|
||||||
|
.OrderByDescending(r => r.GrandTotal)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record UsageGaugeRowViewModel(
|
||||||
|
string Label,
|
||||||
|
double Percent,
|
||||||
|
string Severity,
|
||||||
|
DateTimeOffset? ResetsAt,
|
||||||
|
int? ThresholdPercent)
|
||||||
|
{
|
||||||
|
public bool IsWarnSeverity => !string.Equals(Severity, "normal", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public string ResetText => ResetsAt is { } r ? Loc.T("modals.usageMonitor.resetIn", FormatRemaining(r)) : "";
|
||||||
|
|
||||||
|
// Matches the gauge card's inner track width in the view (240 card width - 12*2 padding).
|
||||||
|
private const double GaugeTrackWidthPx = 216;
|
||||||
|
|
||||||
|
public double ThresholdMarkerLeftPx =>
|
||||||
|
ThresholdPercent is { } t ? GaugeTrackWidthPx * Math.Clamp(t, 0, 100) / 100.0 : 0;
|
||||||
|
|
||||||
|
private static string FormatRemaining(DateTimeOffset resetsAt)
|
||||||
|
{
|
||||||
|
var remaining = resetsAt - DateTimeOffset.UtcNow;
|
||||||
|
if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
|
||||||
|
var hours = (int)remaining.TotalHours;
|
||||||
|
var minutes = remaining.Minutes;
|
||||||
|
return hours > 0
|
||||||
|
? Loc.T("usage.pill.durationHoursMinutes", hours, minutes)
|
||||||
|
: Loc.T("usage.pill.durationMinutes", minutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ModelUsageDisplayRow(
|
||||||
|
string Model,
|
||||||
|
long ClaudeDoInputTokens,
|
||||||
|
long ClaudeDoOutputTokens,
|
||||||
|
long ClaudeDoCacheTokens,
|
||||||
|
long OtherInputTokens,
|
||||||
|
long OtherOutputTokens,
|
||||||
|
long OtherCacheTokens)
|
||||||
|
{
|
||||||
|
public double SharePercent { get; init; }
|
||||||
|
public long ClaudeDoTotal => ClaudeDoInputTokens + ClaudeDoOutputTokens + ClaudeDoCacheTokens;
|
||||||
|
public long OtherTotal => OtherInputTokens + OtherOutputTokens + OtherCacheTokens;
|
||||||
|
public long GrandTotal => ClaudeDoTotal + OtherTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record TaskUsageDisplayRow(
|
||||||
|
string TaskId,
|
||||||
|
string TaskTitle,
|
||||||
|
string ListName,
|
||||||
|
string? Model,
|
||||||
|
int Runs,
|
||||||
|
long TokensIn,
|
||||||
|
long TokensOut)
|
||||||
|
{
|
||||||
|
public long TotalTokens => TokensIn + TokensOut;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
@@ -37,7 +38,13 @@ public sealed partial class WorkerConnectionModalViewModel : ViewModelBase
|
|||||||
if (path is null) return;
|
if (path is null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
// See IslandsShellViewModel.UpdateNow: an inherited CWD inside the install dir
|
||||||
|
// makes the installer block its own app\ rename.
|
||||||
|
Process.Start(new ProcessStartInfo(path)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = Path.GetTempPath(),
|
||||||
|
});
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
catch { /* nothing useful to show */ }
|
catch { /* nothing useful to show */ }
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed);
|
StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed);
|
||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesOverview.cleanupFailedDetailed", ex.Message); }
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +242,16 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
if (row.IsRunning) { StatusMessage = Loc.T("vm.worktreesOverview.cannotForceRunning"); return; }
|
if (row.IsRunning) { StatusMessage = Loc.T("vm.worktreesOverview.cannotForceRunning"); return; }
|
||||||
if (ConfirmAction is not null && !await ConfirmAction($"Force remove worktree for '{row.TaskTitle}'? This deletes the directory and branch.")) return;
|
if (ConfirmAction is not null && !await ConfirmAction($"Force remove worktree for '{row.TaskTitle}'? This deletes the directory and branch.")) return;
|
||||||
|
|
||||||
var result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
|
ForceRemoveResultDto? result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = Loc.T("vm.worktreesOverview.forceRemoveFailedDetailed", ex.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result is null || !result.Removed)
|
if (result is null || !result.Removed)
|
||||||
{
|
{
|
||||||
StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
|
StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>Backs the footer/Mission-Control-header usage pill. One instance is shared by both hosts.</summary>
|
||||||
|
public sealed partial class UsagePillViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private UsageSnapshotDto? _snapshot;
|
||||||
|
|
||||||
|
/// <summary>Host wires this to open the usage-monitor modal (not built yet — the command is a no-op until then).</summary>
|
||||||
|
public event Action? OpenMonitorRequested;
|
||||||
|
|
||||||
|
public UsagePillViewModel(IWorkerClient worker)
|
||||||
|
{
|
||||||
|
_worker = worker;
|
||||||
|
_worker.UsageUpdatedEvent += OnUsageUpdated;
|
||||||
|
_ = LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task LoadAsync()
|
||||||
|
{
|
||||||
|
var snapshot = await _worker.GetUsageSnapshotAsync();
|
||||||
|
if (snapshot is not null)
|
||||||
|
Snapshot = snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUsageUpdated(UsageSnapshotDto snapshot) => Snapshot = snapshot;
|
||||||
|
|
||||||
|
partial void OnSnapshotChanged(UsageSnapshotDto? value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Text));
|
||||||
|
OnPropertyChanged(nameof(IsWarn));
|
||||||
|
OnPropertyChanged(nameof(IsBlocked));
|
||||||
|
OnPropertyChanged(nameof(IsStale));
|
||||||
|
OnPropertyChanged(nameof(Tooltip));
|
||||||
|
OnPropertyChanged(nameof(ShowNormalDot));
|
||||||
|
OnPropertyChanged(nameof(ShowWarnDot));
|
||||||
|
OnPropertyChanged(nameof(ShowStaleDot));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text => BuildText(Snapshot);
|
||||||
|
|
||||||
|
public bool IsBlocked => Snapshot?.IsGateBlocked == true;
|
||||||
|
|
||||||
|
public bool IsStale => Snapshot?.IsStale == true;
|
||||||
|
|
||||||
|
public bool IsWarn => Snapshot is { } s &&
|
||||||
|
((s.FiveHourPercent is { } five && five >= s.FiveHourThresholdPct - 10) ||
|
||||||
|
(s.SevenDayPercent is { } seven && seven >= s.SevenDayThresholdPct - 10));
|
||||||
|
|
||||||
|
// Visual dot priority: blocked > stale > warn > normal — mirrors the connection pill's
|
||||||
|
// mutually-exclusive dot Ellipses, since Blocked/Stale/Warn aren't mutually exclusive states.
|
||||||
|
public bool ShowNormalDot => !IsBlocked && !IsStale && !IsWarn;
|
||||||
|
public bool ShowWarnDot => IsWarn && !IsBlocked && !IsStale;
|
||||||
|
public bool ShowStaleDot => IsStale && !IsBlocked;
|
||||||
|
|
||||||
|
public string Tooltip => BuildTooltip(Snapshot);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenMonitor() => OpenMonitorRequested?.Invoke();
|
||||||
|
|
||||||
|
private static string BuildText(UsageSnapshotDto? s)
|
||||||
|
{
|
||||||
|
if (s is null) return Loc.T("usage.pill.empty");
|
||||||
|
|
||||||
|
var five = s.FiveHourPercent;
|
||||||
|
var seven = s.SevenDayPercent;
|
||||||
|
if (five is not null && seven is not null)
|
||||||
|
return Loc.T("usage.pill.bothFormat", FormatPct(five.Value), FormatPct(seven.Value));
|
||||||
|
if (five is not null)
|
||||||
|
return Loc.T("usage.pill.fiveHourFormat", FormatPct(five.Value));
|
||||||
|
if (seven is not null)
|
||||||
|
return Loc.T("usage.pill.sevenDayFormat", FormatPct(seven.Value));
|
||||||
|
return Loc.T("usage.pill.empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatPct(double v) => Math.Round(v).ToString("0");
|
||||||
|
|
||||||
|
private static string BuildTooltip(UsageSnapshotDto? s)
|
||||||
|
{
|
||||||
|
if (s is null) return Loc.T("usage.pill.empty");
|
||||||
|
|
||||||
|
var lines = new List<string>();
|
||||||
|
if (s.FiveHourResetsAt is { } fiveReset)
|
||||||
|
lines.Add(Loc.T("usage.pill.resetIn", Loc.T("usage.pill.fiveHourLabel"), FormatRemaining(fiveReset)));
|
||||||
|
if (s.SevenDayResetsAt is { } sevenReset)
|
||||||
|
lines.Add(Loc.T("usage.pill.resetIn", Loc.T("usage.pill.sevenDayLabel"), FormatRemaining(sevenReset)));
|
||||||
|
if (s.IsGateBlocked && !string.IsNullOrEmpty(s.GateReason))
|
||||||
|
lines.Add(Loc.T("usage.pill.blockedReason", s.GateReason));
|
||||||
|
if (s.IsStale)
|
||||||
|
{
|
||||||
|
var stamp = s.FetchedAtUtc?.ToLocalTime().ToString("HH:mm") ?? "?";
|
||||||
|
lines.Add(Loc.T("usage.pill.stale", stamp));
|
||||||
|
if (!string.IsNullOrEmpty(s.LastError))
|
||||||
|
lines.Add(Loc.T("usage.pill.lastError", s.LastError));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.Count > 0 ? string.Join(Environment.NewLine, lines) : Loc.T("usage.pill.empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatRemaining(DateTimeOffset resetsAt)
|
||||||
|
{
|
||||||
|
var remaining = resetsAt - DateTimeOffset.UtcNow;
|
||||||
|
if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
|
||||||
|
var hours = (int)remaining.TotalHours;
|
||||||
|
var minutes = remaining.Minutes;
|
||||||
|
return hours > 0
|
||||||
|
? Loc.T("usage.pill.durationHoursMinutes", hours, minutes)
|
||||||
|
: Loc.T("usage.pill.durationMinutes", minutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Controls.UsagePill"
|
||||||
|
x:DataType="vm:UsagePillViewModel">
|
||||||
|
<Button Command="{Binding OpenMonitorCommand}"
|
||||||
|
Background="Transparent" BorderThickness="0" Padding="0"
|
||||||
|
Cursor="Hand" VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="{Binding Tooltip}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
|
<Ellipse Width="7" Height="7" Fill="{DynamicResource TextDimBrush}"
|
||||||
|
IsVisible="{Binding ShowNormalDot}"/>
|
||||||
|
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusReviewBrush}"
|
||||||
|
IsVisible="{Binding ShowWarnDot}"/>
|
||||||
|
<Ellipse Width="7" Height="7" Fill="{DynamicResource StatusErrorBrush}"
|
||||||
|
IsVisible="{Binding IsBlocked}"/>
|
||||||
|
<Ellipse Width="7" Height="7" Fill="{DynamicResource TextMuteBrush}"
|
||||||
|
IsVisible="{Binding ShowStaleDot}"/>
|
||||||
|
<TextBlock Classes="eyebrow"
|
||||||
|
Text="{Binding Text, Converter={StaticResource UpperCase}}"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Controls;
|
||||||
|
|
||||||
|
public partial class UsagePill : UserControl
|
||||||
|
{
|
||||||
|
public UsagePill() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar"
|
x:Class="ClaudeDo.Ui.Views.Islands.Detail.TaskHeaderBar"
|
||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||||
|
|
||||||
<!-- Column 0: id badge + editable title -->
|
<!-- Column 0: id badge + editable title -->
|
||||||
<StackPanel Grid.Column="0" Spacing="0">
|
<StackPanel Grid.Column="0" Spacing="0">
|
||||||
@@ -27,18 +27,8 @@
|
|||||||
Padding="0"/>
|
Padding="0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Column 1: pick up in terminal (review / failed) -->
|
<!-- Column 1: trash button (not running) -->
|
||||||
<Button Grid.Column="1" Classes="icon-btn"
|
<Button Grid.Column="1" Classes="icon-btn"
|
||||||
Command="{Binding PickUpInTerminalCommand}"
|
|
||||||
ToolTip.Tip="{loc:Tr details.pickUpInTerminalTip}"
|
|
||||||
IsVisible="{Binding CanPickUpInTerminal}"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="6,0,0,0">
|
|
||||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="14" Height="14"/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Column 2: trash button (not running) -->
|
|
||||||
<Button Grid.Column="2" Classes="icon-btn"
|
|
||||||
Command="{Binding DeleteTaskCommand}"
|
Command="{Binding DeleteTaskCommand}"
|
||||||
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
|
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
|
||||||
IsVisible="{Binding !IsRunning}"
|
IsVisible="{Binding !IsRunning}"
|
||||||
@@ -48,8 +38,8 @@
|
|||||||
Foreground="{DynamicResource BloodBrush}"/>
|
Foreground="{DynamicResource BloodBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Column 2: skull button (running) -->
|
<!-- Column 1: skull button (running) -->
|
||||||
<Button Grid.Column="2" Classes="icon-btn"
|
<Button Grid.Column="1" Classes="icon-btn"
|
||||||
Command="{Binding StopCommand}"
|
Command="{Binding StopCommand}"
|
||||||
ToolTip.Tip="{loc:Tr details.killSessionTip}"
|
ToolTip.Tip="{loc:Tr details.killSessionTip}"
|
||||||
IsVisible="{Binding IsRunning}"
|
IsVisible="{Binding IsRunning}"
|
||||||
@@ -59,8 +49,8 @@
|
|||||||
Foreground="{DynamicResource BloodBrush}"/>
|
Foreground="{DynamicResource BloodBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Column 3: gear button with agent settings flyout -->
|
<!-- Column 2: gear button with agent settings flyout -->
|
||||||
<Button Grid.Column="3" Classes="icon-btn"
|
<Button Grid.Column="2" Classes="icon-btn"
|
||||||
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
||||||
IsEnabled="{Binding AgentSettings.IsEnabled}"
|
IsEnabled="{Binding AgentSettings.IsEnabled}"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
|
|||||||
@@ -78,11 +78,29 @@
|
|||||||
<SelectableTextBlock Text="{Binding Roadblocks}"
|
<SelectableTextBlock Text="{Binding Roadblocks}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Foreground="{DynamicResource TextDimBrush}"/>
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding RoadblockReplyDraft, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
PlaceholderText="{loc:Tr details.roadblockReply.placeholder}"
|
||||||
|
IsEnabled="{Binding CanReplyToRoadblock}"
|
||||||
|
AcceptsReturn="False">
|
||||||
|
<TextBox.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Enter" Command="{Binding SendRoadblockReplyCommand}"/>
|
||||||
|
</TextBox.KeyBindings>
|
||||||
|
</TextBox>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Content="{loc:Tr details.roadblockReply.send}"
|
||||||
|
Command="{Binding SendRoadblockReplyCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Classes="meta"
|
||||||
|
Text="{loc:Tr details.roadblockReply.noSession}"
|
||||||
|
IsVisible="{Binding !CanReplyToRoadblock}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Question prompt (AskUser): answer the running task's question inline, mirroring
|
<!-- Question prompt (AskUser): answer the running task's question inline. -->
|
||||||
the Mission Control banner. State is shared via the same TaskMonitorViewModel. -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
IsVisible="{Binding Monitor.HasPendingQuestion}"
|
IsVisible="{Binding Monitor.HasPendingQuestion}"
|
||||||
Margin="0,0,0,10" Padding="12,8"
|
Margin="0,0,0,10" Padding="12,8"
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
IsVisible="{Binding DropHintAbove}"/>
|
IsVisible="{Binding DropHintAbove}"/>
|
||||||
|
|
||||||
<Border Grid.Row="1" Classes="list-item" Classes.active="{Binding IsActive}"
|
<Border Grid.Row="1" Classes="list-item" Classes.active="{Binding IsActive}"
|
||||||
|
Classes.drop-target="{Binding IsTaskDropTarget}"
|
||||||
Tapped="OnItemTapped"
|
Tapped="OnItemTapped"
|
||||||
DragDrop.AllowDrop="True"
|
DragDrop.AllowDrop="True"
|
||||||
DragDrop.DragOver="OnListDragOver"
|
DragDrop.DragOver="OnListDragOver"
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="{loc:Tr tasks.ctxOpenConPtySession}"
|
<MenuItem Header="{loc:Tr tasks.ctxOpenConPtySession}"
|
||||||
Click="OnOpenConPtySessionClick"/>
|
Click="OnOpenConPtySessionClick"/>
|
||||||
<MenuItem Header="{loc:Tr tasks.ctxPickUpInTerminal}"
|
|
||||||
Click="OnPickUpInTerminalClick"
|
|
||||||
IsVisible="{Binding CanPickUpInTerminal}"/>
|
|
||||||
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
|
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
|
||||||
Click="OnOpenPlanningSessionClick"
|
Click="OnOpenPlanningSessionClick"
|
||||||
IsVisible="{Binding CanOpenPlanningSession}"/>
|
IsVisible="{Binding CanOpenPlanningSession}"/>
|
||||||
|
|||||||
@@ -82,12 +82,6 @@ public partial class TaskRowView : UserControl
|
|||||||
vm.OpenConPtySessionCommand.Execute(row);
|
vm.OpenConPtySessionCommand.Execute(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPickUpInTerminalClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.PickUpInTerminalCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public partial class TasksIslandView : UserControl
|
|||||||
private bool _dragArmed;
|
private bool _dragArmed;
|
||||||
private bool _dragging;
|
private bool _dragging;
|
||||||
|
|
||||||
|
// The list row (in the Lists island) currently highlighted as a drop target while dragging.
|
||||||
|
private ListNavItemViewModel? _hintedList;
|
||||||
|
|
||||||
public TasksIslandView()
|
public TasksIslandView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -170,6 +173,7 @@ public partial class TasksIslandView : UserControl
|
|||||||
{
|
{
|
||||||
_drag.MoveTo(this.PointToScreen(e.GetPosition(this)));
|
_drag.MoveTo(this.PointToScreen(e.GetPosition(this)));
|
||||||
UpdateReorderHint(e, topLevel);
|
UpdateReorderHint(e, topLevel);
|
||||||
|
UpdateListDropHint(e, topLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +209,17 @@ public partial class TasksIslandView : UserControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Released over another row in the same user list → reorder.
|
// 2) Released over another user list in the Lists island → move the task there.
|
||||||
|
if (DataContext is TasksIslandViewModel vmMove
|
||||||
|
&& ListItemUnder(e, topLevel) is { } targetList
|
||||||
|
&& targetList.Kind == ListKind.User
|
||||||
|
&& targetList.Id != vmMove.CurrentListId)
|
||||||
|
{
|
||||||
|
await vmMove.MoveTaskToListAsync(row, targetList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Released over another row in the same user list → reorder.
|
||||||
if (DataContext is TasksIslandViewModel vm && vm.CanReorder)
|
if (DataContext is TasksIslandViewModel vm && vm.CanReorder)
|
||||||
{
|
{
|
||||||
var targetButton = RowButtonAt(e, topLevel);
|
var targetButton = RowButtonAt(e, topLevel);
|
||||||
@@ -218,7 +232,7 @@ public partial class TasksIslandView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Anywhere else → cancel; EndDrag already restored the source row.
|
// 4) Anywhere else → cancel; EndDrag already restored the source row.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
||||||
@@ -229,6 +243,7 @@ public partial class TasksIslandView : UserControl
|
|||||||
if (!_dragArmed && !_dragging) return;
|
if (!_dragArmed && !_dragging) return;
|
||||||
if (_pressRow is not null) _pressRow.IsDragging = false;
|
if (_pressRow is not null) _pressRow.IsDragging = false;
|
||||||
if (DataContext is TasksIslandViewModel vm) vm.ClearDropHints();
|
if (DataContext is TasksIslandViewModel vm) vm.ClearDropHints();
|
||||||
|
ClearListDropHint();
|
||||||
_drag.End();
|
_drag.End();
|
||||||
ResetPressState();
|
ResetPressState();
|
||||||
}
|
}
|
||||||
@@ -237,6 +252,7 @@ public partial class TasksIslandView : UserControl
|
|||||||
{
|
{
|
||||||
if (_pressRow is not null) _pressRow.IsDragging = false;
|
if (_pressRow is not null) _pressRow.IsDragging = false;
|
||||||
if (DataContext is TasksIslandViewModel vm) vm.ClearDropHints();
|
if (DataContext is TasksIslandViewModel vm) vm.ClearDropHints();
|
||||||
|
ClearListDropHint();
|
||||||
_drag.End();
|
_drag.End();
|
||||||
if (_dragging) e.Pointer.Capture(null);
|
if (_dragging) e.Pointer.Capture(null);
|
||||||
ResetPressState();
|
ResetPressState();
|
||||||
@@ -295,6 +311,44 @@ public partial class TasksIslandView : UserControl
|
|||||||
return button?.DataContext is TaskRowViewModel ? button : null;
|
return button?.DataContext is TaskRowViewModel ? button : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The list-row Border under the cursor (Lists island), found the same way as RowButtonAt but
|
||||||
|
// walking up to a Border with the "list-item" style class (mirrors ListsIslandView's own
|
||||||
|
// FindListItemBorder) — the task drag has the pointer captured here, so a DragEventArgs never
|
||||||
|
// reaches the Lists island's own DragDrop.DragOver/Drop handlers.
|
||||||
|
private static ListNavItemViewModel? ListItemUnder(PointerEventArgs e, TopLevel topLevel)
|
||||||
|
{
|
||||||
|
var pt = e.GetPosition((Visual)topLevel);
|
||||||
|
Visual? v = topLevel.InputHitTest(pt) as Visual;
|
||||||
|
while (v is not null)
|
||||||
|
{
|
||||||
|
if (v is Border b && b.Classes.Contains("list-item"))
|
||||||
|
return b.DataContext as ListNavItemViewModel;
|
||||||
|
v = v.GetVisualParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live drop-hint while dragging over a candidate target list row.
|
||||||
|
private void UpdateListDropHint(PointerEventArgs e, TopLevel topLevel)
|
||||||
|
{
|
||||||
|
if (DataContext is not TasksIslandViewModel vm) { ClearListDropHint(); return; }
|
||||||
|
|
||||||
|
var target = ListItemUnder(e, topLevel);
|
||||||
|
if (target is null || target.Kind != ListKind.User || target.Id == vm.CurrentListId)
|
||||||
|
target = null;
|
||||||
|
|
||||||
|
if (ReferenceEquals(_hintedList, target)) return;
|
||||||
|
if (_hintedList is not null) _hintedList.IsTaskDropTarget = false;
|
||||||
|
_hintedList = target;
|
||||||
|
if (_hintedList is not null) _hintedList.IsTaskDropTarget = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearListDropHint()
|
||||||
|
{
|
||||||
|
if (_hintedList is not null) _hintedList.IsTaskDropTarget = false;
|
||||||
|
_hintedList = null;
|
||||||
|
}
|
||||||
|
|
||||||
// The Mission Control view model whose window contains the release point, if any.
|
// The Mission Control view model whose window contains the release point, if any.
|
||||||
private static MissionControlViewModel? MissionControlUnder(PixelPoint screen)
|
private static MissionControlViewModel? MissionControlUnder(PixelPoint screen)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||||
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
||||||
|
xmlns:controls="using:ClaudeDo.Ui.Views.Controls"
|
||||||
xmlns:converters="using:ClaudeDo.Ui.Converters"
|
xmlns:converters="using:ClaudeDo.Ui.Converters"
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:Class="ClaudeDo.Ui.Views.MainWindow"
|
x:Class="ClaudeDo.Ui.Views.MainWindow"
|
||||||
@@ -65,6 +66,8 @@
|
|||||||
Command="{Binding RestartWorkerCommand}"/>
|
Command="{Binding RestartWorkerCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
||||||
Command="{Binding CheckForUpdatesCommand}"/>
|
Command="{Binding CheckForUpdatesCommand}"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.usageMonitor}"
|
||||||
|
Command="{Binding OpenUsageMonitorCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
@@ -184,6 +187,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- Left: usage pill (click opens the usage monitor, wired in a later subtask) -->
|
||||||
|
<controls:UsagePill DockPanel.Dock="Left"
|
||||||
|
DataContext="{Binding UsagePill}"
|
||||||
|
Margin="14,0,0,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
|
||||||
<!-- Right: worker log line — click to open the Log Visualizer overlay -->
|
<!-- Right: worker log line — click to open the Log Visualizer overlay -->
|
||||||
<Button DockPanel.Dock="Right"
|
<Button DockPanel.Dock="Right"
|
||||||
Command="{Binding OpenLogVisualizerCommand}"
|
Command="{Binding OpenLogVisualizerCommand}"
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||||
xmlns:vmi="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:vmm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
xmlns:vmm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
||||||
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
||||||
|
xmlns:controls="using:ClaudeDo.Ui.Views.Controls"
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:DataType="vm:MissionControlViewModel"
|
x:DataType="vm:MissionControlViewModel"
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.MissionControlView">
|
x:Class="ClaudeDo.Ui.Views.MissionControl.MissionControlView">
|
||||||
<UserControl.DataTemplates>
|
<UserControl.DataTemplates>
|
||||||
<!-- Polymorphic pane templates: both the grid ItemsControl and the focus-mode TabControl
|
|
||||||
resolve per-item content through these (no explicit ItemTemplate on either). -->
|
|
||||||
<DataTemplate DataType="vmi:TaskMonitorViewModel">
|
|
||||||
<mc:MonitorPaneView Margin="6" />
|
|
||||||
</DataTemplate>
|
|
||||||
<DataTemplate DataType="vmm:ConPtyPaneViewModel">
|
<DataTemplate DataType="vmm:ConPtyPaneViewModel">
|
||||||
<mc:ConPtyPaneView Margin="6" />
|
<mc:ConPtyPaneView Margin="6" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</UserControl.DataTemplates>
|
</UserControl.DataTemplates>
|
||||||
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}"
|
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}">
|
||||||
DragDrop.AllowDrop="True">
|
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
@@ -32,6 +26,9 @@
|
|||||||
LetterSpacing="1.4" VerticalAlignment="Center" />
|
LetterSpacing="1.4" VerticalAlignment="Center" />
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
|
<controls:UsagePill DataContext="{Binding UsagePill}"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
<Button Classes="icon-btn"
|
<Button Classes="icon-btn"
|
||||||
Click="OnNewSessionClicked"
|
Click="OnNewSessionClicked"
|
||||||
ToolTip.Tip="{loc:Tr missionControl.newSession}">
|
ToolTip.Tip="{loc:Tr missionControl.newSession}">
|
||||||
@@ -50,9 +47,6 @@
|
|||||||
<PathIcon Data="{StaticResource Icon.Settings}" Width="15" Height="15"
|
<PathIcon Data="{StaticResource Icon.Settings}" Width="15" Height="15"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"/>
|
Foreground="{DynamicResource TextMuteBrush}"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Classes="btn"
|
|
||||||
Content="{loc:Tr missionControl.clearFinished}"
|
|
||||||
Command="{Binding ClearFinishedCommand}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -73,21 +67,35 @@
|
|||||||
<ItemsControl ItemsSource="{Binding Queued}">
|
<ItemsControl ItemsSource="{Binding Queued}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:QueuedTaskViewModel">
|
<DataTemplate x:DataType="vm:QueuedTaskViewModel">
|
||||||
<Border Margin="0,0,0,4" Padding="8,6"
|
<Button Command="{Binding OpenInAppCommand}"
|
||||||
Background="{DynamicResource SurfaceBrush}"
|
Padding="0" Margin="0,0,0,4"
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
Background="Transparent" BorderThickness="0"
|
||||||
BorderThickness="1" CornerRadius="6">
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||||
<StackPanel Spacing="2">
|
Cursor="Hand">
|
||||||
<TextBlock Text="{Binding Title}"
|
<Panel>
|
||||||
TextTrimming="CharacterEllipsis"
|
<Border Background="{DynamicResource SurfaceBrush}"
|
||||||
ToolTip.Tip="{Binding Title}"
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
Foreground="{DynamicResource TextDimBrush}" />
|
BorderThickness="1" CornerRadius="6" />
|
||||||
<TextBlock Classes="meta"
|
<Border Background="{DynamicResource RunningTintBrush}"
|
||||||
Text="{loc:Tr missionControl.blocked}"
|
BorderBrush="{DynamicResource RunningTintBorderBrush}"
|
||||||
IsVisible="{Binding IsBlocked}"
|
BorderThickness="1" CornerRadius="6"
|
||||||
Foreground="{DynamicResource AmberBrush}" />
|
IsVisible="{Binding IsRunning}" />
|
||||||
</StackPanel>
|
<StackPanel Margin="8,6" Spacing="2">
|
||||||
</Border>
|
<TextBlock Text="{Binding Title}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip.Tip="{Binding Title}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}" />
|
||||||
|
<TextBlock Classes="meta"
|
||||||
|
Text="{loc:Tr missionControl.blocked}"
|
||||||
|
IsVisible="{Binding IsBlocked}"
|
||||||
|
Foreground="{DynamicResource AmberBrush}" />
|
||||||
|
<TextBlock Classes="meta"
|
||||||
|
Text="{loc:Tr missionControl.running}"
|
||||||
|
IsVisible="{Binding IsRunning}"
|
||||||
|
Foreground="{DynamicResource StatusRunningBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
</Button>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
using System.Linq;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
namespace ClaudeDo.Ui.Views.MissionControl;
|
||||||
|
|
||||||
public partial class MissionControlView : UserControl
|
public partial class MissionControlView : UserControl
|
||||||
{
|
{
|
||||||
// Shared with MonitorPaneView (the drag source).
|
|
||||||
public static readonly DataFormat<string> PaneFormat =
|
|
||||||
DataFormat.CreateStringApplicationFormat("claudedo-monitor-pane");
|
|
||||||
|
|
||||||
public MissionControlView()
|
public MissionControlView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AddHandler(DragDrop.DragOverEvent, OnPaneDragOver);
|
|
||||||
AddHandler(DragDrop.DropEvent, OnPaneDrop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
|
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
|
||||||
@@ -38,31 +28,4 @@ public partial class MissionControlView : UserControl
|
|||||||
|
|
||||||
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
|
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPaneDragOver(object? sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
var dt = e.DataTransfer;
|
|
||||||
e.DragEffects = (dt?.Contains(PaneFormat) ?? false) ? DragDropEffects.Move : DragDropEffects.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPaneDrop(object? sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not MissionControlViewModel vm) return;
|
|
||||||
var dt = e.DataTransfer;
|
|
||||||
if (dt is null) return;
|
|
||||||
|
|
||||||
// A pane dragged within the grid → reorder. (Drag-to-queue from the main app now arrives
|
|
||||||
// via the custom ghost drag's screen hit-test, not an OLE drop.)
|
|
||||||
var draggedId = dt.TryGetValue(PaneFormat);
|
|
||||||
if (string.IsNullOrEmpty(draggedId)) return;
|
|
||||||
if (e.Source is not Avalonia.Visual src) return;
|
|
||||||
|
|
||||||
var targetPane = src.FindAncestorOfType<MonitorPaneView>();
|
|
||||||
if (targetPane?.DataContext is not TaskMonitorViewModel target) return;
|
|
||||||
|
|
||||||
var dragged = vm.Monitors.FirstOrDefault(m => m.SubscribedTaskId == draggedId);
|
|
||||||
if (dragged is null) return;
|
|
||||||
|
|
||||||
vm.MoveMonitor(dragged, target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
|
||||||
x:DataType="vm:TaskMonitorViewModel"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.MonitorPaneView">
|
|
||||||
<Border Classes="monitor-pane"
|
|
||||||
DragDrop.AllowDrop="True"
|
|
||||||
Classes.mon-review="{Binding IsWaitingForReview}"
|
|
||||||
Classes.mon-done="{Binding IsDone}"
|
|
||||||
Classes.mon-roadblock="{Binding HasRoadblock}"
|
|
||||||
Classes.mon-failed="{Binding IsFailed}"
|
|
||||||
BorderThickness="1" CornerRadius="10" ClipToBounds="True">
|
|
||||||
<DockPanel LastChildFill="True">
|
|
||||||
|
|
||||||
<!-- Header: per-task action icons -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
Background="{DynamicResource Surface2Brush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="6,3"
|
|
||||||
PointerPressed="OnHeaderPressed">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="2"
|
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center">
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding OpenInAppCommand}"
|
|
||||||
ToolTip.Tip="{loc:Tr missionControl.openInApp}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding DetachCommand}"
|
|
||||||
ToolTip.Tip="{Binding DetachTooltip}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.WinMax}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="title-ctrl"
|
|
||||||
Command="{Binding CancelTaskCommand}"
|
|
||||||
IsVisible="{Binding IsRunning}"
|
|
||||||
ToolTip.Tip="{loc:Tr missionControl.cancel}">
|
|
||||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="12" Height="12"/>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Roadblock banner (amber) -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding HasRoadblock}"
|
|
||||||
Background="{DynamicResource RoadblockTintBrush}"
|
|
||||||
BorderBrush="{DynamicResource AmberBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,6">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<PathIcon Data="{StaticResource Icon.Warning}"
|
|
||||||
Foreground="{DynamicResource AmberBrush}"
|
|
||||||
Width="13" Height="13" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Classes="meta" Text="{Binding Roadblocks}"
|
|
||||||
Foreground="{DynamicResource AmberBrush}"
|
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Failure banner (red) -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding ShowRoadblock}"
|
|
||||||
Background="{DynamicResource ErrorTintBrush}"
|
|
||||||
BorderBrush="{DynamicResource BloodBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,6">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<PathIcon Data="{StaticResource Icon.Warning}"
|
|
||||||
Foreground="{DynamicResource BloodBrush}"
|
|
||||||
Width="13" Height="13" VerticalAlignment="Center" />
|
|
||||||
<TextBlock Classes="meta" Text="{Binding RoadblockMessage}"
|
|
||||||
Foreground="{DynamicResource BloodBrush}"
|
|
||||||
TextWrapping="Wrap" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Question prompt (AskUser): answer a running task's question inline -->
|
|
||||||
<Border DockPanel.Dock="Top"
|
|
||||||
IsVisible="{Binding HasPendingQuestion}"
|
|
||||||
Background="{DynamicResource AccentSoftBrush}"
|
|
||||||
BorderBrush="{DynamicResource AccentBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="12,8">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="meta"
|
|
||||||
Text="{loc:Tr missionControl.question.title}"
|
|
||||||
Foreground="{DynamicResource AccentBrush}" FontWeight="SemiBold" />
|
|
||||||
<TextBlock Text="{Binding PendingQuestion}" TextWrapping="Wrap"
|
|
||||||
Foreground="{DynamicResource TextBrush}" />
|
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
|
||||||
<TextBox Grid.Column="0"
|
|
||||||
Text="{Binding AnswerDraft, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
PlaceholderText="{loc:Tr missionControl.question.placeholder}"
|
|
||||||
AcceptsReturn="False">
|
|
||||||
<TextBox.KeyBindings>
|
|
||||||
<KeyBinding Gesture="Enter" Command="{Binding SubmitAnswerCommand}" />
|
|
||||||
</TextBox.KeyBindings>
|
|
||||||
</TextBox>
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Content="{loc:Tr missionControl.question.send}"
|
|
||||||
Command="{Binding SubmitAnswerCommand}" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Console body: reuse SessionTerminalView -->
|
|
||||||
<islands:SessionTerminalView
|
|
||||||
Entries="{Binding Log}"
|
|
||||||
Label="{Binding DisplayTitle}"
|
|
||||||
IsRunning="{Binding IsRunning}"
|
|
||||||
IsDone="{Binding IsDone}"
|
|
||||||
IsFailed="{Binding IsFailed}" />
|
|
||||||
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
|
||||||
|
|
||||||
public partial class MonitorPaneView : UserControl
|
|
||||||
{
|
|
||||||
public MonitorPaneView() => InitializeComponent();
|
|
||||||
|
|
||||||
private async void OnHeaderPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not TaskMonitorViewModel m || m.SubscribedTaskId is not { } id) return;
|
|
||||||
if (e.Source is not Avalonia.Visual src) return;
|
|
||||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
|
||||||
|
|
||||||
// Don't start a drag when the press landed on a header action button.
|
|
||||||
var button = src as Button ?? src.FindAncestorOfType<Button>();
|
|
||||||
if (button is not null) return;
|
|
||||||
|
|
||||||
var data = new DataTransfer();
|
|
||||||
data.Add(DataTransferItem.Create(MissionControlView.PaneFormat, id));
|
|
||||||
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
|
||||||
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.MissionControl.TaskMonitorWindow"
|
|
||||||
x:DataType="vm:TaskMonitorViewModel"
|
|
||||||
Title="{Binding DisplayTitle}"
|
|
||||||
Width="560" Height="620" MinWidth="360" MinHeight="320"
|
|
||||||
Background="{DynamicResource VoidBrush}"
|
|
||||||
Icon="avares://ClaudeDo.Ui/Assets/ClaudeTask.ico">
|
|
||||||
<mc:MonitorPaneView />
|
|
||||||
</Window>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
|
||||||
|
|
||||||
public partial class TaskMonitorWindow : Window
|
|
||||||
{
|
|
||||||
public TaskMonitorWindow() => InitializeComponent();
|
|
||||||
|
|
||||||
protected override void OnOpened(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnOpened(e);
|
|
||||||
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
|
|
||||||
vm.CloseWindowRequested = Close;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
|
|
||||||
vm.CloseWindowRequested = null;
|
|
||||||
base.OnClosed(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -83,6 +83,20 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- VERIFICATION -->
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="section-label" Text="{loc:Tr modals.listSettings.sectionVerify}"/>
|
||||||
|
<Border Classes="section">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.verifyCommand}"/>
|
||||||
|
<TextBox Text="{Binding VerifyCommand, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
PlaceholderText="{loc:Tr modals.listSettings.verifyCommandPlaceholder}"/>
|
||||||
|
<TextBlock Text="{loc:Tr modals.listSettings.verifyCommandHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
Command="{Binding RefreshCommand}"/>
|
Command="{Binding RefreshCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Last 30 min, oldest-first -->
|
<!-- Last 30 min, newest-first -->
|
||||||
<Border Classes="terminal" Margin="14,0,14,14">
|
<Border Classes="terminal" Margin="14,0,14,14">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Visible" AllowAutoHide="False" Padding="10,8">
|
<ScrollViewer VerticalScrollBarVisibility="Visible" AllowAutoHide="False" Padding="10,8">
|
||||||
<ItemsControl ItemsSource="{Binding Rows}">
|
<ItemsControl ItemsSource="{Binding Rows}">
|
||||||
|
|||||||
@@ -149,6 +149,25 @@
|
|||||||
<TextBlock Text="{loc:Tr settings.general.maxParallelExecutionsHint}"
|
<TextBlock Text="{loc:Tr settings.general.maxParallelExecutionsHint}"
|
||||||
Opacity="0.6" FontSize="12"/>
|
Opacity="0.6" FontSize="12"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.usageGateHeading}"/>
|
||||||
|
<TextBlock Text="{loc:Tr settings.general.usageGateHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.usageGateFiveHourPct}"/>
|
||||||
|
<NumericUpDown Value="{Binding General.UsageGateFiveHourPct, Mode=TwoWay}"
|
||||||
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.usageGateSevenDayPct}"/>
|
||||||
|
<NumericUpDown Value="{Binding General.UsageGateSevenDayPct, Mode=TwoWay}"
|
||||||
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
||||||
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||||
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
|
xmlns:conv="using:Avalonia.Data.Converters"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Modals.UsageMonitorModalView"
|
||||||
|
x:DataType="vm:UsageMonitorModalViewModel"
|
||||||
|
Title="{loc:Tr modals.usageMonitor.windowTitle}"
|
||||||
|
Width="920" Height="680" MinWidth="700" MinHeight="480"
|
||||||
|
CanResize="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{DynamicResource SurfaceBrush}"
|
||||||
|
WindowDecorations="BorderOnly"
|
||||||
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
|
ExtendClientAreaTitleBarHeightHint="-1">
|
||||||
|
<Window.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
||||||
|
</Window.KeyBindings>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="ProgressBar.usage-gauge">
|
||||||
|
<Setter Property="Height" Value="10"/>
|
||||||
|
<Setter Property="CornerRadius" Value="4"/>
|
||||||
|
<Setter Property="Minimum" Value="0"/>
|
||||||
|
<Setter Property="Maximum" Value="100"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ProgressBar.usage-gauge.warn">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource StatusReviewBrush}"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<ctl:ModalShell Title="{loc:Tr modals.usageMonitor.title}" CloseCommand="{Binding CloseCommand}">
|
||||||
|
<DockPanel>
|
||||||
|
|
||||||
|
<!-- Stale / gate bands -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="20,16,20,0">
|
||||||
|
<Border IsVisible="{Binding IsStale}"
|
||||||
|
Background="{DynamicResource ReviewTintBrush}"
|
||||||
|
BorderBrush="{DynamicResource StatusReviewBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="12,8">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Classes="meta" Text="{Binding StaleBandText}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{Binding LastError}" IsVisible="{Binding LastError, Converter={x:Static conv:StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.staleGateHint}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border IsVisible="{Binding IsGateBlocked}"
|
||||||
|
Background="{DynamicResource ErrorTintBrush}"
|
||||||
|
BorderBrush="{DynamicResource StatusErrorBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="12,8">
|
||||||
|
<TextBlock Classes="meta" Text="{Binding GateBandText}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Gauges -->
|
||||||
|
<ItemsControl DockPanel.Dock="Top" Margin="20,12,20,0" ItemsSource="{Binding GaugeRows}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:UsageGaugeRowViewModel">
|
||||||
|
<Border Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="12,10" Margin="0,0,10,10" Width="240">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Classes="eyebrow" Text="{Binding Label}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{Binding Percent, StringFormat={}{0:0}%}" HorizontalAlignment="Right"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Height="10">
|
||||||
|
<ProgressBar Classes="usage-gauge" Classes.warn="{Binding IsWarnSeverity}" Value="{Binding Percent}"/>
|
||||||
|
<Canvas IsHitTestVisible="False">
|
||||||
|
<Rectangle Canvas.Left="{Binding ThresholdMarkerLeftPx}" Width="2" Height="10"
|
||||||
|
Fill="{DynamicResource TextDimBrush}"
|
||||||
|
IsVisible="{Binding ThresholdPercent, Converter={x:Static conv:ObjectConverters.IsNotNull}}"/>
|
||||||
|
</Canvas>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Classes="meta" Text="{Binding ResetText}" IsVisible="{Binding ResetText, Converter={x:Static conv:StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Margin="20,12,20,0" Classes="meta"
|
||||||
|
Text="{loc:Tr modals.usageMonitor.noGauges}"
|
||||||
|
IsVisible="{Binding !GaugeRows.Count}"/>
|
||||||
|
|
||||||
|
<!-- Period picker -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="20,12,20,0">
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.period}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.last7Days}"
|
||||||
|
Command="{Binding SetPreset7DaysCommand}"/>
|
||||||
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.last30Days}"
|
||||||
|
Command="{Binding SetPreset30DaysCommand}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.fromDate}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:ThemedDatePicker SelectedDate="{Binding StartDate}"/>
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.toDate}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:ThemedDatePicker SelectedDate="{Binding EndDate}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TabControl Padding="20,12" TabStripPlacement="Top">
|
||||||
|
|
||||||
|
<TabItem Header="{loc:Tr modals.usageMonitor.modelUsage}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="meta" Margin="0,16"
|
||||||
|
Text="{loc:Tr modals.usageMonitor.emptyStateHint}"
|
||||||
|
IsVisible="{Binding ModelsEmpty}"/>
|
||||||
|
<Grid ColumnDefinitions="*,80,80,80,80,80,80,60" Margin="12,0,12,4" IsVisible="{Binding !ModelsEmpty}">
|
||||||
|
<TextBlock Grid.Column="0" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnModel}"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnIn}"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOut}"/>
|
||||||
|
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnCache}"/>
|
||||||
|
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherIn}"/>
|
||||||
|
<TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherOut}"/>
|
||||||
|
<TextBlock Grid.Column="6" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherCache}"/>
|
||||||
|
<TextBlock Grid.Column="7" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnShare}"/>
|
||||||
|
</Grid>
|
||||||
|
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8" IsVisible="{Binding !ModelsEmpty}"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding ModelRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ModelUsageDisplayRow">
|
||||||
|
<Grid ColumnDefinitions="*,80,80,80,80,80,80,60" Margin="12,4">
|
||||||
|
<TextBlock Grid.Column="0" Classes="title" Text="{Binding Model}"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding ClaudeDoInputTokens}"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="meta" Text="{Binding ClaudeDoOutputTokens}"/>
|
||||||
|
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding ClaudeDoCacheTokens}"/>
|
||||||
|
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding OtherInputTokens}"/>
|
||||||
|
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding OtherOutputTokens}"/>
|
||||||
|
<TextBlock Grid.Column="6" Classes="meta" Text="{Binding OtherCacheTokens}"/>
|
||||||
|
<TextBlock Grid.Column="7" Classes="meta" Text="{Binding SharePercent, StringFormat={}{0:0}%}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="{loc:Tr modals.usageMonitor.taskUsage}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="meta" Margin="0,16"
|
||||||
|
Text="{loc:Tr modals.usageMonitor.emptyStateHint}"
|
||||||
|
IsVisible="{Binding TasksEmpty}"/>
|
||||||
|
<Grid ColumnDefinitions="*,110,90,60,80,80,80" Margin="12,0,12,4" IsVisible="{Binding !TasksEmpty}">
|
||||||
|
<TextBlock Grid.Column="0" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnTask}"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnList}"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnModelShort}"/>
|
||||||
|
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnRuns}"/>
|
||||||
|
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnIn}"/>
|
||||||
|
<TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOut}"/>
|
||||||
|
<TextBlock Grid.Column="6" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnTotal}"/>
|
||||||
|
</Grid>
|
||||||
|
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8" IsVisible="{Binding !TasksEmpty}"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding TaskRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:TaskUsageDisplayRow">
|
||||||
|
<Grid ColumnDefinitions="*,110,90,60,80,80,80" Margin="12,4">
|
||||||
|
<TextBlock Grid.Column="0" Classes="title" Text="{Binding TaskTitle}"/>
|
||||||
|
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding ListName}"/>
|
||||||
|
<TextBlock Grid.Column="2" Classes="meta" Text="{Binding Model}"/>
|
||||||
|
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding Runs}"/>
|
||||||
|
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding TokensIn}"/>
|
||||||
|
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding TokensOut}"/>
|
||||||
|
<TextBlock Grid.Column="6" Classes="meta" Text="{Binding TotalTokens}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
<Ellipse Classes="spinner" Width="20" Height="20" Margin="0,0,0,16"
|
||||||
|
HorizontalAlignment="Center" DockPanel.Dock="Bottom"
|
||||||
|
IsVisible="{Binding IsBusy}"/>
|
||||||
|
|
||||||
|
</DockPanel>
|
||||||
|
</ctl:ModalShell>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
|
public partial class UsageMonitorModalView : Window
|
||||||
|
{
|
||||||
|
public UsageMonitorModalView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ using Avalonia.Media;
|
|||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Conflicts;
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using ClaudeDo.Ui.Views.Conflicts;
|
using ClaudeDo.Ui.Views.Conflicts;
|
||||||
using ClaudeDo.Ui.Views.MissionControl;
|
using ClaudeDo.Ui.Views.MissionControl;
|
||||||
@@ -59,6 +58,13 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
await dlg.ShowDialog(_owner);
|
await dlg.ShowDialog(_owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm)
|
||||||
|
{
|
||||||
|
var dlg = new UsageMonitorModalView { DataContext = vm };
|
||||||
|
vm.CloseAction = () => dlg.Close();
|
||||||
|
await dlg.ShowDialog(_owner);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
||||||
{
|
{
|
||||||
var dlg = new SettingsModalView { DataContext = vm };
|
var dlg = new SettingsModalView { DataContext = vm };
|
||||||
@@ -146,13 +152,6 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
_missionControl.Activate(); // bring to front / focus
|
_missionControl.Activate(); // bring to front / focus
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed)
|
|
||||||
{
|
|
||||||
var win = new TaskMonitorWindow { DataContext = monitor };
|
|
||||||
win.Closed += (_, _) => onClosed(); // closing re-docks into the grid
|
|
||||||
win.Show(); // modeless, independent
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> ConfirmAsync(string message)
|
public Task<bool> ConfirmAsync(string message)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ ASP.NET Core hosted service that executes tasks via Claude CLI in isolated envir
|
|||||||
Worker/
|
Worker/
|
||||||
State/ — TaskStateService + TransitionResult (sole owner of Status/PlanningPhase/BlockedBy writes)
|
State/ — TaskStateService + TransitionResult (sole owner of Status/PlanningPhase/BlockedBy writes)
|
||||||
Queue/ — IQueueWaker, IQueuePicker, QueueService (BackgroundService), OverrideSlotService, RunCancellationRegistry (taskId → running-run CTS; lets TaskStateService.CancelAsync kill the process of a cancelled task/child without a DI cycle)
|
Queue/ — IQueueWaker, IQueuePicker, QueueService (BackgroundService), OverrideSlotService, RunCancellationRegistry (taskId → running-run CTS; lets TaskStateService.CancelAsync kill the process of a cancelled task/child without a DI cycle)
|
||||||
Lifecycle/ — StaleTaskRecovery, TaskResetService, TaskMergeService, ClaudeCliPreflight, OrphanRecovery, PlanningLineageRecovery, AttachmentOrphanRecovery (startup sweep: deletes any `attachments/<taskId>/` dirs whose task no longer exists)
|
Lifecycle/ — StaleTaskRecovery, TaskResetService, TaskMergeService, VerifyCommandRunner (IVerifyCommandRunner — spawns a list's optional post-merge verify command via `cmd.exe /c`), ClaudeCliPreflight, OrphanRecovery, PlanningLineageRecovery, AttachmentOrphanRecovery (startup sweep: deletes any `attachments/<taskId>/` dirs whose task no longer exists)
|
||||||
Worktrees/ — WorktreeMaintenanceService
|
Worktrees/ — WorktreeMaintenanceService
|
||||||
Agents/ — AgentFileService, DefaultAgentSeeder
|
Agents/ — AgentFileService, DefaultAgentSeeder
|
||||||
Runner/ — TaskRunner + Claude CLI integration; TaskRunMcpService/TaskRunMcpContext/TaskRunTokenRegistry (in-task MCP wired during execution)
|
Runner/ — TaskRunner + Claude CLI integration; TaskRunMcpService/TaskRunMcpContext/TaskRunTokenRegistry (in-task MCP wired during execution)
|
||||||
Planning/ — PlanningSessionManager, PlanningChainCoordinator, PlanningMcpService, PlanningMergeOrchestrator, PlanningAggregator, PlanningSessionContext/PlanningTokenAuth/PlanningMcpContextAccessor, WindowsTerminalLauncher (ITerminalLauncher) — wt launcher for planning sessions + pick-up-in-terminal
|
Planning/ — PlanningSessionManager, PlanningChainCoordinator, PlanningMcpService, PlanningMergeOrchestrator, PlanningAggregator, PlanningSessionContext/PlanningTokenAuth/PlanningMcpContextAccessor, WindowsTerminalLauncher (ITerminalLauncher) — wt launcher for planning sessions
|
||||||
Refine/ — RefineRunner + RefinePrompt (hub `RefineTask`; broadcasts RefineStarted/RefineFinished)
|
Refine/ — RefineRunner + RefinePrompt (hub `RefineTask`; broadcasts RefineStarted/RefineFinished)
|
||||||
External/ — ExternalMcpService + sibling tool classes
|
External/ — ExternalMcpService + sibling tool classes
|
||||||
Config/ — WorkerConfig
|
Config/ — WorkerConfig
|
||||||
@@ -21,6 +21,7 @@ Worker/
|
|||||||
Report/ — ClaudeHistoryReader, WeekReportPromptBuilder, WeekReportService; interfaces in Report/Interfaces/
|
Report/ — ClaudeHistoryReader, WeekReportPromptBuilder, WeekReportService; interfaces in Report/Interfaces/
|
||||||
Prime/ — daily-prep ("Prime Claude"): PrimeScheduler (BackgroundService), PrimeRunner (runs the daily prep), DailyPrepPrompt (fixed prompt + CLI args + LogPath() helper), NextDueCalculator, PrimeScheduleSignal; interfaces in Prime/Interfaces/ (IPrimeRunner, IPrimeClock, IPrimeScheduleSignal, IPrimeBroadcaster)
|
Prime/ — daily-prep ("Prime Claude"): PrimeScheduler (BackgroundService), PrimeRunner (runs the daily prep), DailyPrepPrompt (fixed prompt + CLI args + LogPath() helper), NextDueCalculator, PrimeScheduleSignal; interfaces in Prime/Interfaces/ (IPrimeRunner, IPrimeClock, IPrimeScheduleSignal, IPrimeBroadcaster)
|
||||||
Online/ — optional Online Inbox sync: OnlineInboxConfig (config record), Dtos (RemoteList/RemoteTask/MirrorTask), IOnlineInboxApi, OnlineInboxApiClient (typed HttpClient, bearer auth, HTTPS guard), OnlineTokenStore (DPAPI refresh-token store, Windows-only), StaticTokenAuthProvider (default/test IOnlineAuthProvider), ZitadelAuthProvider (OIDC discovery + refresh-token flow), OnlineSyncService (BackgroundService: reconcile loop), OnlineBacklog (Idle-backlog filter/query); interface in Online/Interfaces/ (IOnlineAuthProvider)
|
Online/ — optional Online Inbox sync: OnlineInboxConfig (config record), Dtos (RemoteList/RemoteTask/MirrorTask), IOnlineInboxApi, OnlineInboxApiClient (typed HttpClient, bearer auth, HTTPS guard), OnlineTokenStore (DPAPI refresh-token store, Windows-only), StaticTokenAuthProvider (default/test IOnlineAuthProvider), ZitadelAuthProvider (OIDC discovery + refresh-token flow), OnlineSyncService (BackgroundService: reconcile loop), OnlineBacklog (Idle-backlog filter/query); interface in Online/Interfaces/ (IOnlineAuthProvider)
|
||||||
|
Usage/ — OAuth usage monitor: UsageModels (UsageBucket/UsageLimitRow/UsageSnapshot), ClaudeOAuthUsageClient (reads the access token Claude Code keeps fresh at `~/.claude/.credentials.json`, calls `GET https://api.anthropic.com/api/oauth/usage`; defensive parsing — missing/null buckets → null, missing `limits` → empty list; never logs the token), UsageState (threadsafe singleton; a failed poll never overwrites the last good snapshot, only sets `LastError`), UsageMonitorService (BackgroundService, polls on `usage_poll_interval_seconds`, one poll at startup, logs a failure at most once per distinct error message, broadcasts `HubBroadcaster.UsageUpdated` after every tick via `UsageSnapshotBuilder`), UsageSnapshotBuilder (builds the Hub-facing `UsageSnapshotDto` from `UsageState` + `IUsageGate` + `AppSettings` thresholds — the one place `WorkerHub.GetUsageSnapshot` and `UsageMonitorService` share the stale/threshold/gate logic), TranscriptUsageReader (aggregates Claude Code transcript token usage from `~/.claude/projects/**/*.jsonl` by date/model/scope (ClaudeDo vs Other), deduped by requestId, with a per-file length+mtime cache), UsageGate (reads `UsageState` + `AppSettings.UsageGateFiveHourPct`/`UsageGateSevenDayPct`, returns a `UsageGateDecision(IsBlocked, Reason)`; `Utilization` from `UsageBucket` is already a 0–100 percent, compared directly against the threshold with `>=`; threshold `0` = that bucket never gates; fail-open — no snapshot yet, a failed last poll, or a settings-read error all resolve to not-blocked); interfaces in Usage/Interfaces/ (IUsageClient, ITranscriptUsageReader, IUsageGate)
|
||||||
```
|
```
|
||||||
|
|
||||||
Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `Interfaces/` subfolder within their area; the namespace stays the area namespace.
|
Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `Interfaces/` subfolder within their area; the namespace stays the area namespace.
|
||||||
@@ -29,18 +30,19 @@ Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `
|
|||||||
|
|
||||||
- **Program.cs** — loads config, inits schema, registers DI, configures SignalR on `/hub`, binds to `127.0.0.1:47821`
|
- **Program.cs** — loads config, inits schema, registers DI, configures SignalR on `/hub`, binds to `127.0.0.1:47821`
|
||||||
- **TaskStateService** — only component that writes `Status`, `PlanningPhase`, `BlockedByTaskId`. All transitions return a `TransitionResult` (no exceptions on invalid moves). Wakes the queue and broadcasts `TaskUpdated` automatically; advances the planning chain on child terminal transitions.
|
- **TaskStateService** — only component that writes `Status`, `PlanningPhase`, `BlockedByTaskId`. All transitions return a `TransitionResult` (no exceptions on invalid moves). Wakes the queue and broadcasts `TaskUpdated` automatically; advances the planning chain on child terminal transitions.
|
||||||
- **IQueueWaker / IQueuePicker / QueueService** — waker is a singleton `SemaphoreSlim`; picker performs the atomic `Queued → Running` claim filtered by `BlockedByTaskId IS NULL`, `is_manual = 0` and schedule; QueueService is a thin `BackgroundService` that loops on the waker and dispatches via `TaskRunner`.
|
- **IQueueWaker / IQueuePicker / QueueService** — waker is a singleton `SemaphoreSlim`; picker performs the atomic `Queued → Running` claim filtered by `BlockedByTaskId IS NULL`, `is_manual = 0` and schedule; QueueService is a thin `BackgroundService` that loops on the waker and dispatches via `TaskRunner`. On each loop tick, after computing `maxParallel`, it also asks `IUsageGate.EvaluateAsync` — if blocked, the slot-fill loop is skipped entirely for that tick (already-running slots are untouched; `RunNow`/`ContinueTask`/interactive/planning/daily-prep all bypass the queue and are unaffected). A blocked↔free transition is logged/broadcast (`WorkerLog`, Warn on block / Info on resume) exactly once per change, not on every tick; the 30 s backstop timer re-evaluates the gate on its own even with no wake signal, so the queue self-recovers once usage drops back under the threshold.
|
||||||
- **OverrideSlotService** — owns `RunNow` / `ContinueTask`; goes through `TaskStateService.StartRunningAsync` (caller-driven, serialized by slot lock).
|
- **OverrideSlotService** — owns `RunNow` / `ContinueTask`; goes through `TaskStateService.StartRunningAsync` (caller-driven, serialized by slot lock).
|
||||||
- **StaleTaskRecovery** — startup-only service; calls `TaskStateService.RecoverStaleRunningAsync` to flip orphaned `Running` rows to `Failed`.
|
- **StaleTaskRecovery** — startup-only service; calls `TaskStateService.RecoverStaleRunningAsync` to flip orphaned `Running` rows to `Failed`.
|
||||||
- **External/*** — always-on MCP tools for general Claude sessions, scoped to *starting* and *observing* sessions (no worktree/merge, multi-turn, planning, or app-settings writes). Auth via optional `X-ClaudeDo-Key` header. Registered explicitly in `Program.cs`'s external app via `.WithTools<T>()`. Every optional/filter parameter across these tools must carry a C# default value (e.g. `string? status = null`) — the MCP schema only marks a parameter optional when it has one; nullability alone doesn't do it (`ExternalMcpToolSchemaTests` guards this by reflection). `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so `InvalidOperationException`/`ArgumentException` messages survive as `McpException` — otherwise the SDK's own catch-all replaces any non-`McpException` with a generic "An error occurred invoking 'X'." Organized by concern:
|
- **External/*** — always-on MCP tools for general Claude sessions, scoped to *starting* and *observing* sessions (no worktree/merge, multi-turn, planning, or app-settings writes). Auth via optional `X-ClaudeDo-Key` header. Registered explicitly in `Program.cs`'s external app via `.WithTools<T>()`. Every optional/filter parameter across these tools must carry a C# default value (e.g. `string? status = null`) — the MCP schema only marks a parameter optional when it has one; nullability alone doesn't do it (`ExternalMcpToolSchemaTests` guards this by reflection). `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so `InvalidOperationException`/`ArgumentException` messages survive as `McpException` — otherwise the SDK's own catch-all replaces any non-`McpException` with a generic "An error occurred invoking 'X'." **No external tool returns bare `Task` or a nullable payload directly** — an MCP client can't tell an empty/omitted response apart from a dropped one. Write tools return a small confirmation record (`{ ok/deleted/removed/reset/started: true, <id>, ... }`, e.g. `DeleteListResult`, `RunTaskNowResult`, `ResetFailedTaskResult`, `RemoveAttachmentResult`; `SetListConfigResult`/`SetTaskConfigResult` additionally echo the resulting config so the caller can see which fields were set vs. cleared to null); read tools that may have nothing to return use an explicit `Found`/`Available` flag alongside the nullable payload (`TaskConfigResult`, pre-existing `BatchGetTaskResult`, `TaskLogResult`) instead of returning null outright. Organized by concern:
|
||||||
- `ExternalMcpService` — task CRUD + execution: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`, `UpdateTaskStatus` (`Idle` / `Queued` / `Cancelled` / `Done` — `Cancelled` goes through `TaskStateService.CancelAsync(..., allowFromIdle: true)`, the only caller that opts into cancelling from `Idle`; `PlanningChainCoordinator` relies on `Idle` staying a no-op there by default, since a child parked back to `Idle` mid-chain is a manual opt-out signal; `Done` goes through `TaskStateService.ForceSetStatusAsync` — same unconditional write the UI's "set status freely" affordance uses — but is refused here with an active-worktree task, since that would skip `review_task`'s merge), `GetTaskStatusValues`, `ReviewTask` (`approve` / `reject_rerun` / `reject_park` / `cancel` for a WaitingForReview task; approve is review+merge exactly like the hub's `ApproveReview` — unit merge for parents, worktree merge into optional `targetBranch` for childless tasks, conflicts reported in the `ReviewTaskResult`), `RunTaskNow`, `ContinueTask`, `CancelTask`, `DeleteTask`; worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ListWorktrees`, `CleanupTaskWorktree`
|
- `ExternalMcpService` — task CRUD + execution: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`, `UpdateTaskStatus` (`Idle` / `Queued` / `Cancelled` / `Done` — `Cancelled` goes through `TaskStateService.CancelAsync(..., allowFromIdle: true)`, the only caller that opts into cancelling from `Idle`; `PlanningChainCoordinator` relies on `Idle` staying a no-op there by default, since a child parked back to `Idle` mid-chain is a manual opt-out signal; `Done` goes through `TaskStateService.ForceSetStatusAsync` — same unconditional write the UI's "set status freely" affordance uses — but is refused here with an active-worktree task, since that would skip `review_task`'s merge), `GetTaskStatusValues`, `ReviewTask` (`approve` / `reject_rerun` / `reject_park` / `cancel` for a WaitingForReview task; approve is review+merge exactly like the hub's `ApproveReview` — unit merge for parents, worktree merge into optional `targetBranch` for childless tasks, conflicts reported in the `ReviewTaskResult`), `RunTaskNow`, `ContinueTask`, `CancelTask`, `DeleteTask`; worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ContinueMerge`, `AbortMerge`, `PreviewMerge` (non-destructive `git merge-tree --write-tree` mergeability check for one task's worktree branch against `targetBranch`, default the repo's current branch — status/conflictFiles/changedFileCount plus `behind`; throws a clear error instead of TaskMergeService.PreviewAsync's silent "unavailable" when the task has no worktree, the worktree isn't Active, or the list's working dir is missing), `PreviewMergeSet` (same preview for a batch of task ids plus a file→tasks overlap report built from each task's own diff-stat — a same-file-name hint only, blind to cross-file collisions like the CS0103 case that motivated it; a task that fails to preview gets `error` set and is excluded from the overlap instead of aborting the batch), `RevertMerge` (undoes a previously merged task's merge commit on `targetBranch` via `git revert -m 1` — a new commit, never a reset/rewrite, since the target working directory is shared with other sessions; requires the task to be `Done` with a `Merged` worktree carrying a recorded `WorktreeEntity.MergeCommit` — a task merged before that field existed has none and is refused rather than guessed via `git log`; on success the task returns to `WaitingForReview` and the worktree moves to `Kept`, not `Active` (its directory/branch are typically already gone from the original merge's cleanup) and not `Merged`/`Discarded` (`WorktreeMaintenanceService` sweeps those); a conflicting revert is aborted immediately, no half-resolved state is ever left in the tree), `ListWorktrees`, `CleanupTaskWorktree`
|
||||||
- `BatchMcpTools` — best-effort batch variants that loop the `ExternalMcpService` single-entity methods (sequential — the scoped DbContext is not thread-safe; merge/review stay single-task): `BatchGetTasks`, `BatchAddTasks`, `BatchUpdateTaskStatus`, `BatchCancelTasks`, `BatchDeleteTasks`, `BatchSetMyDay`, `BatchCleanupTaskWorktrees`. Every tool returns a per-item result array ({ id/index, ok, error?, … }) — a failing item never aborts the rest — and rejects batches over 100 items.
|
- `BatchMcpTools` — best-effort batch variants that loop the `ExternalMcpService` single-entity methods (sequential — the scoped DbContext is not thread-safe; merge/review stay single-task): `BatchGetTasks`, `BatchAddTasks`, `BatchUpdateTaskStatus`, `BatchCancelTasks`, `BatchDeleteTasks`, `BatchSetMyDay`, `BatchCleanupTaskWorktrees`. Every tool returns a per-item result array ({ id/index, ok, error?, … }) — a failing item never aborts the rest — and rejects batches over 100 items.
|
||||||
- `ListMcpTools` — `CreateList`, `UpdateList`, `DeleteList`
|
- `ListMcpTools` — `CreateList`, `UpdateList`, `DeleteList`
|
||||||
- `ConfigMcpTools` — `GetListConfig`, `SetListConfig`, `GetTaskConfig`, `SetTaskConfig`
|
- `ConfigMcpTools` — `GetListConfig`, `SetListConfig`, `GetTaskConfig`, `SetTaskConfig`
|
||||||
- `RunHistoryMcpTools` — `ListRuns`, `GetRun`, `GetTaskLog` (latest run's log, tail-capped at 256 KB)
|
- `RunHistoryMcpTools` — `ListRuns`, `GetRun`, `GetTaskLog` (latest run's log, tail-capped at 256 KB)
|
||||||
- `AgentMcpTools` — `ListAgents`
|
- `AgentMcpTools` — `ListAgents`
|
||||||
- `LifecycleMcpTools` — `ResetFailedTask`
|
- `LifecycleMcpTools` — `ResetFailedTask`
|
||||||
- `AppSettingsMcpTools` — `GetAppSettings` (read-only)
|
- `AppSettingsMcpTools` — `GetAppSettings` (read-only; includes `MaxParallelExecutions`)
|
||||||
|
- `TaskWaitMcpTools` — `WaitForTaskChange(taskIds, timeoutSeconds = 60)`: blocks until any given task leaves `Queued`/`Running`, or times out; returns immediately for a task already outside `Queued`/`Running` (unknown ids reported as status `"NotFound"`, also immediate). Implemented as an async DB poll (short-lived `DbContext` per check, 500ms delay between checks, no held connection or busy loop) rather than hooking `HubBroadcaster` — kept deliberately isolated so it can't regress the existing broadcast callers. `timeoutSeconds` is clamped server-side to `TaskWaitMcpTools.MaxTimeoutSeconds` (170s), comfortably under the list handler's `MCP_TOOL_TIMEOUT` (200s, see `InteractiveLaunchSpecService`) so the tool reports `timedOut: true` instead of racing the client's own abort. Replaces the list handler's old "sleep + poll get_task in a loop" Phase 3 instruction (`PromptFiles.MergeHelperDefault`).
|
||||||
- `AttachmentMcpTools` — `AddTaskAttachment(taskId, fileName, textContent?|base64Content?)`, `ListTaskAttachments`, `RemoveTaskAttachment`. Re-attaching the same fileName overwrites; add/remove refuse on a Running task.
|
- `AttachmentMcpTools` — `AddTaskAttachment(taskId, fileName, textContent?|base64Content?)`, `ListTaskAttachments`, `RemoveTaskAttachment`. Re-attaching the same fileName overwrites; add/remove refuse on a Running task.
|
||||||
- `ExternalMcpService` also exposes two daily-prep tools:
|
- `ExternalMcpService` also exposes two daily-prep tools:
|
||||||
- `GetDailyPrepCandidates` — returns Idle, non-blocked tasks in a git repo NOT excluded by `AppSettings.ReportExcludedPaths` and not already `IsMyDay`, plus the current Idle MyDay tasks and `maxTasks` (= `DailyPrepMaxTasks`). Repo-exclusion logic lives in the `DailyPrepFilter` helper (same file).
|
- `GetDailyPrepCandidates` — returns Idle, non-blocked tasks in a git repo NOT excluded by `AppSettings.ReportExcludedPaths` and not already `IsMyDay`, plus the current Idle MyDay tasks and `maxTasks` (= `DailyPrepMaxTasks`). Repo-exclusion logic lives in the `DailyPrepFilter` helper (same file).
|
||||||
@@ -58,6 +60,25 @@ Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `
|
|||||||
- Each stdout line is streamed to the UI via `IPrimeBroadcaster.PrepLineAsync` AND written to `DailyPrepPrompt.LogPath()` = `<appdata>/logs/daily-prep.log` (truncated at the start of each run → last run only). `PrepStarted`/`PrepFinished` events bracket the run.
|
- Each stdout line is streamed to the UI via `IPrimeBroadcaster.PrepLineAsync` AND written to `DailyPrepPrompt.LogPath()` = `<appdata>/logs/daily-prep.log` (truncated at the start of each run → last run only). `PrepStarted`/`PrepFinished` events bracket the run.
|
||||||
- Agentic behaviour: Claude calls `get_daily_prep_candidates`, picks an effort-aware subset capped at `DailyPrepMaxTasks`, and marks them via `set_my_day` (which broadcasts `TaskUpdated` so the UI updates live).
|
- Agentic behaviour: Claude calls `get_daily_prep_candidates`, picks an effort-aware subset capped at `DailyPrepMaxTasks`, and marks them via `set_my_day` (which broadcasts `TaskUpdated` so the UI updates live).
|
||||||
|
|
||||||
|
## Usage Monitor & Gate
|
||||||
|
|
||||||
|
Source: `GET https://api.anthropic.com/api/oauth/usage`, an **undocumented** Anthropic API,
|
||||||
|
authenticated with the Bearer access token Claude Code itself keeps fresh at
|
||||||
|
`~/.claude/.credentials.json` — ClaudeDo reads that token but never refreshes it and never
|
||||||
|
logs it. `UsageMonitorService` polls on `usage_poll_interval_seconds` (default 60s, one poll
|
||||||
|
at startup too) and broadcasts `HubBroadcaster.UsageUpdated` after every tick.
|
||||||
|
|
||||||
|
The gate (`IUsageGate`, thresholds `usage_gate_five_hour_pct`/`usage_gate_seven_day_pct`)
|
||||||
|
pauses **only** the queue's slot-fill loop (new tasks don't start) once `five_hour >=
|
||||||
|
usage_gate_five_hour_pct` or `seven_day >= usage_gate_seven_day_pct`; already-running runs,
|
||||||
|
and `RunNow`/`ContinueTask`/interactive ConPTY/planning sessions/daily-prep all bypass the
|
||||||
|
queue and are unaffected. **Fail-open**: no snapshot yet, a failed last poll, or an
|
||||||
|
app-settings read error all resolve to not-blocked, so an outage of the (undocumented, can
|
||||||
|
change without notice) usage endpoint never blocks work. There's no persistent pause state —
|
||||||
|
recovery is just the queue's 30s backstop timer re-evaluating the gate on its own once usage
|
||||||
|
drops back under the threshold. See `Usage/` in the folder layout above for the component
|
||||||
|
breakdown.
|
||||||
|
|
||||||
## Status Model
|
## Status Model
|
||||||
|
|
||||||
`TaskEntity` carries three orthogonal fields. Lifecycle, planning hierarchy, and chain blocking are no longer conflated.
|
`TaskEntity` carries three orthogonal fields. Lifecycle, planning hierarchy, and chain blocking are no longer conflated.
|
||||||
@@ -100,7 +121,25 @@ that has children, drives `PlanningMergeOrchestrator` (merges the parent worktre
|
|||||||
Active + each `Done` child in order, sets the parent `Done`, and on a mid-merge
|
Active + each `Done` child in order, sets the parent `Done`, and on a mid-merge
|
||||||
conflict pauses for `ContinuePlanningMerge`/`AbortPlanningMerge`). Childless tasks use
|
conflict pauses for `ContinuePlanningMerge`/`AbortPlanningMerge`). Childless tasks use
|
||||||
`TaskMergeService.ApproveAndMergeAsync`. There is no separate "Merge all" entry —
|
`TaskMergeService.ApproveAndMergeAsync`. There is no separate "Merge all" entry —
|
||||||
approve is the single review+merge action. Review transitions live in `TaskStateService`
|
approve is the single review+merge action.
|
||||||
|
|
||||||
|
**Post-merge verify gate.** A list can set `ListConfigEntity.VerifyCommand` (List Settings
|
||||||
|
modal → Verification). Null/blank (the default) = no gate, behavior is bit-identical to
|
||||||
|
before this existed. When set, `TaskMergeService` runs it via `VerifyCommandRunner`
|
||||||
|
(`cmd.exe /c <command>`, 10-minute fixed timeout, output tail-captured) in `list.WorkingDir`
|
||||||
|
right after a successful `MergeNoFfAsync`/`ContinueMergeAsync` and worktree cleanup, but
|
||||||
|
*before* the task is allowed to reach `Done`. Exit 0 → unchanged flow (worktree marked
|
||||||
|
`Merged`, task `Done` if it was `WaitingForReview`). Non-zero exit or a timeout → the git
|
||||||
|
merge is deliberately left in place (no auto-revert — that's a separate, unbuilt feature),
|
||||||
|
the worktree is still marked `Merged` (it's already gone from disk when `removeWorktree`
|
||||||
|
was requested), but the task stays out of `Done` and `MergeResult.Status` comes back
|
||||||
|
`TaskMergeService.StatusVerifyFailed` (`"verify_failed"`) with an output excerpt in
|
||||||
|
`ErrorMessage` — this flows through `MergeResultDto` (hub) and `ReviewTaskResult`
|
||||||
|
(`review_task` MCP tool) unchanged, since both already treat any non-`blocked`/`conflict`
|
||||||
|
status generically. A process-wide `ConcurrentDictionary<string, SemaphoreSlim>` keyed by
|
||||||
|
`list.WorkingDir` serializes `MergeAsync`/`ContinueMergeAsync` (git ops + verify) per repo,
|
||||||
|
so a verify run can't be interrupted by a second merge landing in the same working dir
|
||||||
|
mid-build. Review transitions live in `TaskStateService`
|
||||||
(`SubmitForReviewAsync`, `SubmitForChildrenAsync`, `ApproveReviewAsync`,
|
(`SubmitForReviewAsync`, `SubmitForChildrenAsync`, `ApproveReviewAsync`,
|
||||||
`RejectToQueueAsync`, `RejectToIdleAsync`, `ClearReviewFeedbackAsync`).
|
`RejectToQueueAsync`, `RejectToIdleAsync`, `ClearReviewFeedbackAsync`).
|
||||||
|
|
||||||
@@ -122,7 +161,7 @@ A child that hits a roadblock (fails, or reports `CLAUDEDO_BLOCKED` roadblocks)
|
|||||||
1. Load task + list metadata from DB; resolve config from `list_config` + task-level overrides (model, system_prompt, agent_path)
|
1. Load task + list metadata from DB; resolve config from `list_config` + task-level overrides (model, system_prompt, agent_path)
|
||||||
2. Create worktree (if `WorkingDir` set) or sandbox directory
|
2. Create worktree (if `WorkingDir` set) or sandbox directory
|
||||||
3. Mark task "running", broadcast `TaskStarted`
|
3. Mark task "running", broadcast `TaskStarted`
|
||||||
4. Resolve the effective model (task → list → `AppSettings.DefaultModel`), then take its `ModelPresets` row: the preset supplies `--effort` and the **global** max-turns default (task/list `MaxTurns` overrides still win). Build CLI args via `ClaudeArgsBuilder`; inject attachment absolute paths via `TaskPromptComposer.Compose` (appends a read-only "## Reference files" section); invoke `ClaudeProcess` with task prompt
|
4. Resolve the effective model (task → list → `AppSettings.DefaultModel`), then take its `ModelPresets` row via `ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns)`: the model string is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`, not just the bare `sonnet`/`opus`/`haiku`/`fable` aliases) still hits its alias's preset row; only a truly unrecognized model falls back to a synthesized row using `AppSettings.DefaultMaxTurns` (never a hardcoded number, and never throws — an unknown model must not block a run). The preset supplies `--effort` and the **global** max-turns default (task/list `MaxTurns` overrides still win). Build CLI args via `ClaudeArgsBuilder`; inject attachment absolute paths via `TaskPromptComposer.Compose` (appends a read-only "## Reference files" section); invoke `ClaudeProcess` with task prompt
|
||||||
5. Stream NDJSON output through `StreamAnalyzer`; lines forwarded to log file and SignalR (`TaskMessage`)
|
5. Stream NDJSON output through `StreamAnalyzer`; lines forwarded to log file and SignalR (`TaskMessage`)
|
||||||
6. On success: auto-commit changes (worktree only), store run record, mark "done"
|
6. On success: auto-commit changes (worktree only), store run record, mark "done"
|
||||||
7. On failure: retry once if session ID available (`--resume`), then mark "failed"
|
7. On failure: retry once if session ID available (`--resume`), then mark "failed"
|
||||||
@@ -156,13 +195,14 @@ Each CLI invocation is recorded in the `task_runs` table via `TaskRunRepository`
|
|||||||
- Review/merge: `ApproveReview(taskId, targetBranch) -> MergeResultDto` (childless task: merges its worktree then Done, conflict stays WaitingForReview; task with children: drives `PlanningMergeOrchestrator` to merge the whole unit), `ContinuePlanningMerge` / `AbortPlanningMerge` (resolve a unit-merge conflict), `PreviewMerge(taskId, targetBranch) -> MergePreviewDto` (non-destructive mergeability check), `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `MergeTask`, `GetMergeTargets`
|
- Review/merge: `ApproveReview(taskId, targetBranch) -> MergeResultDto` (childless task: merges its worktree then Done, conflict stays WaitingForReview; task with children: drives `PlanningMergeOrchestrator` to merge the whole unit), `ContinuePlanningMerge` / `AbortPlanningMerge` (resolve a unit-merge conflict), `PreviewMerge(taskId, targetBranch) -> MergePreviewDto` (non-destructive mergeability check), `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `MergeTask`, `GetMergeTargets`
|
||||||
- Single-task conflict resolver (Layer C): `StartConflictMerge`, `GetMergeConflictDocuments` (segments), `WriteConflictResolution`, `ContinueConflictMerge`, `AbortConflictMerge` (service-level `TaskMergeService.ContinueMergeAsync`/`AbortMergeAsync` keep their names)
|
- Single-task conflict resolver (Layer C): `StartConflictMerge`, `GetMergeConflictDocuments` (segments), `WriteConflictResolution`, `ContinueConflictMerge`, `AbortConflictMerge` (service-level `TaskMergeService.ContinueMergeAsync`/`AbortMergeAsync` keep their names)
|
||||||
- Planning sessions: `StartPlanningSession`, `ResumePlanningSession`, `DiscardPlanningSession`, `FinalizePlanningSession`, `QueuePlanningSubtasks`, `GetPendingDraftCount`, `GetPlanningAggregate` (per-subtask diffs), `BuildPlanningIntegrationBranch` (combined diff)
|
- Planning sessions: `StartPlanningSession`, `ResumePlanningSession`, `DiscardPlanningSession`, `FinalizePlanningSession`, `QueuePlanningSubtasks`, `GetPendingDraftCount`, `GetPlanningAggregate` (per-subtask diffs), `BuildPlanningIntegrationBranch` (combined diff)
|
||||||
- Interactive sessions (embedded ConPTY, UI process): `ResumeTaskInTerminal` (pick-up-in-terminal), `GetInteractiveLaunchSpec`, `GetAdHocLaunchSpec`. **Every** ConPTY spec that `InteractiveLaunchSpecService` builds leads with `--effort <level>` from the relevant model's preset (task/list model for a task session, `PlanningAlias` for planning, list config for the list handler, global default for ad-hoc). `--model` is deliberately NOT forced on an interactive session — the user can still switch models in the TUI.
|
- Interactive sessions (embedded ConPTY, UI process): `GetInteractiveLaunchSpec`, `GetAdHocLaunchSpec`, `GetMergeHelperLaunchSpec`, `CreateMergeHelperTask` (creates the ClaudeDo task that owns a list-handler run — `Idle`/`IsManual=true`, `HandlerBaseCommit` stamped to the list repo's current HEAD via `InteractiveLaunchSpecService.CreateMergeHelperTaskAsync` — called by the UI before it opens the task-based ConPTY tile), `SubmitTaskForReview` (branches on whether the task has a `WorktreeEntity`: with one, commits it and moves on; without one, it's a worktree-less list-handler host task and it just stamps `HandlerHeadCommit` to the list repo's current HEAD — both paths then flip the task Idle/Failed → WaitingForReview). Every ConPTY spec that `InteractiveLaunchSpecService` builds passes `--effort <level>` from the relevant model's preset (task/list model for a task session, `PlanningAlias` for planning, list config for the list handler, global default for ad-hoc) — it leads the args except for a fresh task session with a brief, where `--add-dir <sessionDir>` must come first so `--effort` (a single-value flag) can sit directly before the positional kickoff (see below). `--model` is deliberately NOT forced on an interactive session — the user can still switch models in the TUI. The list-handler spec (`BuildForMergeHelperAsync`) uses `--permission-mode auto` so it runs unattended; the `--allowedTools` allowlist (`mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill`) remains the security boundary. **No ConPTY path ever passes task free-text (title/description/brief) as a CLI argument** — every one of them (task session, planning start/resume, list handler) writes it to a file first and hands claude a single-line kickoff pointing at that file, exposed via `--add-dir`. Reason: the ConPTY host flattens `Args` into one command line to spawn the process, and claude re-splits that line on whitespace, so any token starting with `-` (e.g. `->`, `--abort`) in real task text would be misread as an unknown option — and a raw multi-line positional prompt truncates at its first newline regardless. A fresh task session's brief lives at `~/.todo-app/task-sessions/<taskId>/brief.md` (`InteractiveLaunchSpecService.BuildFreshTaskArgsAsync`); a task with neither title nor description skips the file and the positional arg entirely.
|
||||||
- Worktrees: `CleanupFinishedWorktrees`, `ResetAllWorktrees`, `GetWorktreesOverview`, `SetWorktreeState`, `ForceRemoveWorktree`
|
- Worktrees: `CleanupFinishedWorktrees`, `ResetAllWorktrees`, `GetWorktreesOverview`, `SetWorktreeState`, `ForceRemoveWorktree`
|
||||||
- Agents/settings/lists: `GetAgents`, `RefreshAgents`, `RestoreDefaultAgents`, `GetAppSettings`, `UpdateAppSettings`, `UpdateList`, `UpdateListConfig`, `GetListConfig`, `UpdateTaskAgentSettings`
|
- Agents/settings/lists: `GetAgents`, `RefreshAgents`, `RestoreDefaultAgents`, `GetAppSettings`, `UpdateAppSettings`, `UpdateList`, `UpdateListConfig`, `GetListConfig`, `UpdateTaskAgentSettings`
|
||||||
- Reports/notes/prep: `GetWeekReport`, `GenerateWeekReport`, `GetDailyNotes`, `AddDailyNote`, `UpdateDailyNote`, `DeleteDailyNote`, `RunDailyPrepNow`, `ClearMyDay`, `GetLastPrepLog`, `ListPrimeSchedules`, `UpsertPrimeSchedule`, `DeletePrimeSchedule`
|
- Reports/notes/prep: `GetWeekReport`, `GenerateWeekReport`, `GetDailyNotes`, `AddDailyNote`, `UpdateDailyNote`, `DeleteDailyNote`, `RunDailyPrepNow`, `ClearMyDay`, `GetLastPrepLog`, `ListPrimeSchedules`, `UpsertPrimeSchedule`, `DeletePrimeSchedule`
|
||||||
- Diagnostics: `GetRecentLogs` (last 30 min of buffered log records, all levels, for the Log Visualizer overlay)
|
- Diagnostics: `GetRecentLogs` (last 30 min of buffered log records, all levels, for the Log Visualizer overlay)
|
||||||
|
- Usage: `GetUsageSnapshot() -> UsageSnapshotDto` (built by `UsageSnapshotBuilder` from `UsageState` + `IUsageGate` + `AppSettings` gate thresholds; percentages/limits/`FetchedAtUtc` null and `IsStale=true` when no snapshot has landed yet; `IsStale` also trips on a failed last poll or a snapshot older than 3× `usage_poll_interval_seconds`), `GetModelUsage(from, to) -> IReadOnlyList<ModelUsageRowDto>` (thin wrapper over `ITranscriptUsageReader.ReadAsync`), `GetTaskUsage(from, to) -> IReadOnlyList<TaskUsageRowDto>` (top consumers from `task_runs` joined to task/list, grouped per task — `Runs`/summed `TokensIn`/`TokensOut` (null token columns count as 0, never dropped), `Model` from that task's most recent run — sorted by total tokens descending, capped at 100)
|
||||||
|
|
||||||
**HubBroadcaster** events: `TaskStarted`, `TaskFinished`, `TaskMessage`, `WorktreeUpdated`, `TaskUpdated`, `RunCreated`, `ListUpdated`, `WorkerLog`, `PrimeFired`, `PrepStarted`, `PrepLine`, `PrepFinished`, `PlanningMergeStarted`, `PlanningSubtaskMerged`, `PlanningMergeConflict`, `PlanningMergeAborted`, `PlanningCompleted`, `RefineStarted`, `RefineFinished`
|
**HubBroadcaster** events: `TaskStarted`, `TaskFinished`, `TaskMessage`, `WorktreeUpdated`, `TaskUpdated`, `RunCreated`, `ListUpdated`, `WorkerLog`, `PrimeFired`, `PrepStarted`, `PrepLine`, `PrepFinished`, `PlanningMergeStarted`, `PlanningSubtaskMerged`, `PlanningMergeConflict`, `PlanningMergeAborted`, `PlanningCompleted`, `RefineStarted`, `RefineFinished`, `UsageUpdated` (carries the same `UsageSnapshotDto` as `GetUsageSnapshot`; `UsageMonitorService` fires it after every poll cycle, success or failure, via the shared `UsageSnapshotBuilder`)
|
||||||
|
|
||||||
`WorkerLog` carries two sources: the hand-curated business events (`_broadcaster.WorkerLog(...)` in TaskRunner/TaskMergeService/TaskResetService) **and** every Serilog **Warn/Error** event, re-broadcast by `BroadcastLogSink` (deduped within a 120 s per-message window; SignalR plumbing source-contexts filtered to avoid feedback loops). The sink also buffers **all** levels into `LogRingBuffer` for `GetRecentLogs`.
|
`WorkerLog` carries two sources: the hand-curated business events (`_broadcaster.WorkerLog(...)` in TaskRunner/TaskMergeService/TaskResetService) **and** every Serilog **Warn/Error** event, re-broadcast by `BroadcastLogSink` (deduped within a 120 s per-message window; SignalR plumbing source-contexts filtered to avoid feedback loops). The sink also buffers **all** levels into `LogRingBuffer` for `GetRecentLogs`.
|
||||||
|
|
||||||
@@ -180,8 +220,9 @@ Loaded from `~/.todo-app/worker.config.json`:
|
|||||||
- `poll_interval_seconds` (int, default 60)
|
- `poll_interval_seconds` (int, default 60)
|
||||||
- `zitadel.authority`, `zitadel.client_id`, `zitadel.scopes` — used by `ZitadelAuthProvider` (OIDC discovery + refresh-token flow)
|
- `zitadel.authority`, `zitadel.client_id`, `zitadel.scopes` — used by `ZitadelAuthProvider` (OIDC discovery + refresh-token flow)
|
||||||
- The refresh token is NOT in this file — stored encrypted via DPAPI at `~/.todo-app/online-inbox.token`
|
- The refresh token is NOT in this file — stored encrypted via DPAPI at `~/.todo-app/online-inbox.token`
|
||||||
|
- `usage_poll_interval_seconds` (default 60, clamped to a minimum of 15 on load) — poll interval for `UsageMonitorService`
|
||||||
|
|
||||||
Per-list config (`list_config` in DB) provides defaults for `model`, `system_prompt`, `agent_path`; tasks can override each individually. Task-generating MCP tools (`AddTask`, planning `CreateChildTask`, `SuggestImprovement`) accept an optional `model` (alias-validated via `ModelRegistry.NormalizeAlias` — `haiku`/`sonnet`/`opus`, blank = inherit) so Claude assigns the cheapest capable model at creation time; the planning/system/improvement prompts instruct it to do so (`ModelRegistry.ByCostAscending` = the cost order).
|
Per-list config (`list_config` in DB) provides defaults for `model`, `system_prompt`, `agent_path`; tasks can override each individually. Task-generating MCP tools (`AddTask`, planning `CreateChildTask`, `SuggestImprovement`) accept an optional `model` (alias-validated via `ModelRegistry.NormalizeAlias` — `haiku`/`sonnet`/`opus`, blank = inherit) so Claude assigns the cheapest capable model at creation time; the planning/system/improvement prompts instruct it to do so (`ModelRegistry.ByCostAscending` = the cost order). Planning's `CreateChildTask` additionally accepts an optional `maxTurns` (positive int; `0`/negative rejected with `ArgumentException`, null = inherit list/global default) so the planner can raise the turn budget for a subtask it knows will run long; `SuggestImprovement`/`AddTask` do not expose it.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
|
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.1.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ public sealed class WorkerConfig
|
|||||||
[JsonPropertyName("online_inbox")]
|
[JsonPropertyName("online_inbox")]
|
||||||
public OnlineInboxConfig OnlineInbox { get; set; } = new();
|
public OnlineInboxConfig OnlineInbox { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Poll interval for the OAuth usage monitor. Clamped to a minimum of 15s on load.</summary>
|
||||||
|
[JsonPropertyName("usage_poll_interval_seconds")]
|
||||||
|
public int UsagePollIntervalSeconds { get; set; } = 60;
|
||||||
|
|
||||||
public static string DefaultConfigPath =>
|
public static string DefaultConfigPath =>
|
||||||
Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||||
|
|
||||||
@@ -71,6 +75,7 @@ public sealed class WorkerConfig
|
|||||||
cfg.SandboxRoot = Paths.Expand(cfg.SandboxRoot);
|
cfg.SandboxRoot = Paths.Expand(cfg.SandboxRoot);
|
||||||
cfg.LogRoot = Paths.Expand(cfg.LogRoot);
|
cfg.LogRoot = Paths.Expand(cfg.LogRoot);
|
||||||
cfg.CentralWorktreeRoot = Paths.Expand(cfg.CentralWorktreeRoot);
|
cfg.CentralWorktreeRoot = Paths.Expand(cfg.CentralWorktreeRoot);
|
||||||
|
cfg.UsagePollIntervalSeconds = Math.Max(15, cfg.UsagePollIntervalSeconds);
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -8,6 +8,7 @@ namespace ClaudeDo.Worker.External;
|
|||||||
|
|
||||||
public sealed record AppSettingsReadDto(
|
public sealed record AppSettingsReadDto(
|
||||||
string DefaultModel, int DefaultMaxTurns, string DefaultPermissionMode,
|
string DefaultModel, int DefaultMaxTurns, string DefaultPermissionMode,
|
||||||
|
int MaxParallelExecutions,
|
||||||
string WorktreeStrategy, string? CentralWorktreeRoot,
|
string WorktreeStrategy, string? CentralWorktreeRoot,
|
||||||
bool WorktreeAutoCleanupEnabled, int WorktreeAutoCleanupDays);
|
bool WorktreeAutoCleanupEnabled, int WorktreeAutoCleanupDays);
|
||||||
|
|
||||||
@@ -18,13 +19,14 @@ public sealed class AppSettingsMcpTools
|
|||||||
|
|
||||||
public AppSettingsMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory) => _dbFactory = dbFactory;
|
public AppSettingsMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory) => _dbFactory = dbFactory;
|
||||||
|
|
||||||
[McpServerTool, Description("Read the worker's app-level defaults (model, max turns, permission mode, worktree strategy). Read-only.")]
|
[McpServerTool, Description("Read the worker's app-level defaults (model, max turns, permission mode, max parallel execution slots, worktree strategy). Read-only.")]
|
||||||
public async Task<AppSettingsReadDto> GetAppSettings(CancellationToken cancellationToken)
|
public async Task<AppSettingsReadDto> GetAppSettings(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
var row = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
var row = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
return new AppSettingsReadDto(
|
return new AppSettingsReadDto(
|
||||||
row.DefaultModel, row.DefaultMaxTurns, row.DefaultPermissionMode,
|
row.DefaultModel, row.DefaultMaxTurns, row.DefaultPermissionMode,
|
||||||
|
row.MaxParallelExecutions,
|
||||||
row.WorktreeStrategy, row.CentralWorktreeRoot,
|
row.WorktreeStrategy, row.CentralWorktreeRoot,
|
||||||
row.WorktreeAutoCleanupEnabled, row.WorktreeAutoCleanupDays);
|
row.WorktreeAutoCleanupEnabled, row.WorktreeAutoCleanupDays);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -10,6 +10,7 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
public sealed record AttachmentDto(string FileName, long ByteSize, DateTime CreatedAt);
|
public sealed record AttachmentDto(string FileName, long ByteSize, DateTime CreatedAt);
|
||||||
|
public sealed record RemoveAttachmentResult(bool Removed, string TaskId, string FileName);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class AttachmentMcpTools
|
public sealed class AttachmentMcpTools
|
||||||
@@ -103,8 +104,8 @@ public sealed class AttachmentMcpTools
|
|||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Remove a single attachment from a task. Deletes both the file on disk and the database record. " +
|
"Remove a single attachment from a task. Deletes both the file on disk and the database record. " +
|
||||||
"Refuses if the task is currently Running — cancel it first.")]
|
"Refuses if the task is currently Running — cancel it first. Returns { removed: true, taskId, fileName } on success.")]
|
||||||
public async Task RemoveTaskAttachment(
|
public async Task<RemoveAttachmentResult> RemoveTaskAttachment(
|
||||||
string taskId, string fileName, CancellationToken ct = default)
|
string taskId, string fileName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, ct)
|
var task = await _tasks.GetByIdAsync(taskId, ct)
|
||||||
@@ -115,5 +116,6 @@ public sealed class AttachmentMcpTools
|
|||||||
_store.DeleteFile(taskId, fileName);
|
_store.DeleteFile(taskId, fileName);
|
||||||
await _attachments.DeleteAsync(taskId, fileName, ct);
|
await _attachments.DeleteAsync(taskId, fileName, ct);
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return new RemoveAttachmentResult(true, taskId, fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-12
@@ -7,6 +7,9 @@ using ModelContextProtocol.Server;
|
|||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
|
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
|
||||||
|
public sealed record TaskConfigResult(bool Found, TaskConfigDto? Config);
|
||||||
|
public sealed record SetListConfigResult(bool Ok, string ListId, TaskConfigDto? Config);
|
||||||
|
public sealed record SetTaskConfigResult(bool Ok, string TaskId, TaskConfigDto? Config);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class ConfigMcpTools
|
public sealed class ConfigMcpTools
|
||||||
@@ -22,15 +25,20 @@ public sealed class ConfigMcpTools
|
|||||||
_broadcaster = broadcaster;
|
_broadcaster = broadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Get a list's default config (model, system prompt, agent path). Returns null if no config is set.")]
|
[McpServerTool, Description("Get a list's default config (model, system prompt, agent path). Returns { found: false, config: null } if no config is set.")]
|
||||||
public async Task<TaskConfigDto?> GetListConfig(string listId, CancellationToken cancellationToken)
|
public async Task<TaskConfigResult> GetListConfig(string listId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
||||||
return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns);
|
return cfg is null
|
||||||
|
? new TaskConfigResult(false, null)
|
||||||
|
: new TaskConfigResult(true, new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns));
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Set a list's default model/system prompt/agent path/max turns. Passing all four as null clears the list config.")]
|
[McpServerTool, Description(
|
||||||
public async Task SetListConfig(
|
"Set a list's default model/system prompt/agent path/max turns. Passing all four as null clears the list " +
|
||||||
|
"config. Returns { ok, listId, config } — config is null when the config was cleared, otherwise it echoes " +
|
||||||
|
"the fields that were set (a field is null there if it was individually left unset/cleared).")]
|
||||||
|
public async Task<SetListConfigResult> SetListConfig(
|
||||||
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
||||||
int? maxTurns = null, CancellationToken cancellationToken = default)
|
int? maxTurns = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -41,36 +49,52 @@ public sealed class ConfigMcpTools
|
|||||||
var sp = systemPrompt.NullIfBlank();
|
var sp = systemPrompt.NullIfBlank();
|
||||||
var ap = agentPath.NullIfBlank();
|
var ap = agentPath.NullIfBlank();
|
||||||
|
|
||||||
|
TaskConfigDto? config;
|
||||||
if (m is null && sp is null && ap is null && maxTurns is null)
|
if (m is null && sp is null && ap is null && maxTurns is null)
|
||||||
|
{
|
||||||
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
||||||
|
config = null;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
await _lists.SetConfigAsync(new ListConfigEntity
|
await _lists.SetConfigAsync(new ListConfigEntity
|
||||||
{
|
{
|
||||||
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
|
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
config = new TaskConfigDto(m, sp, ap, maxTurns);
|
||||||
|
}
|
||||||
|
|
||||||
await _broadcaster.ListUpdated(listId);
|
await _broadcaster.ListUpdated(listId);
|
||||||
|
return new SetListConfigResult(true, listId, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to clear that override.")]
|
[McpServerTool, Description(
|
||||||
public async Task SetTaskConfig(
|
"Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to " +
|
||||||
|
"clear that override. Returns { ok, taskId, config } — config echoes the resulting overrides (a field is " +
|
||||||
|
"null there if it was cleared or never set).")]
|
||||||
|
public async Task<SetTaskConfigResult> SetTaskConfig(
|
||||||
string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
|
||||||
int? maxTurns = null, CancellationToken cancellationToken = default)
|
int? maxTurns = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, ct: cancellationToken);
|
var m = model.NullIfBlank();
|
||||||
|
var sp = systemPrompt.NullIfBlank();
|
||||||
|
var ap = agentPath.NullIfBlank();
|
||||||
|
|
||||||
|
await _tasks.UpdateAgentSettingsAsync(taskId, m, sp, ap, maxTurns, ct: cancellationToken);
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return new SetTaskConfigResult(true, taskId, new TaskConfigDto(m, sp, ap, maxTurns));
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Get per-task config overrides (model/system prompt/agent path/max turns). Returns null if no override is set on this task.")]
|
[McpServerTool, Description("Get per-task config overrides (model/system prompt/agent path/max turns). Returns { found: false, config: null } if no override is set on this task.")]
|
||||||
public async Task<TaskConfigDto?> GetTaskConfig(string taskId, CancellationToken cancellationToken)
|
public async Task<TaskConfigResult> GetTaskConfig(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
if (task.Model is null && task.SystemPrompt is null && task.AgentPath is null && task.MaxTurns is null)
|
if (task.Model is null && task.SystemPrompt is null && task.AgentPath is null && task.MaxTurns is null)
|
||||||
return null;
|
return new TaskConfigResult(false, null);
|
||||||
return new TaskConfigDto(task.Model, task.SystemPrompt, task.AgentPath, task.MaxTurns);
|
return new TaskConfigResult(true, new TaskConfigDto(task.Model, task.SystemPrompt, task.AgentPath, task.MaxTurns));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-13
@@ -21,6 +21,7 @@ public sealed record DeleteTaskResult(bool Deleted, string Id);
|
|||||||
public sealed record CancelTaskResult(bool Cancelled, string Id);
|
public sealed record CancelTaskResult(bool Cancelled, string Id);
|
||||||
public sealed record ReviewTaskResult(TaskDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
public sealed record ReviewTaskResult(TaskDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
||||||
public sealed record StatusValueDto(string Status, string Meaning);
|
public sealed record StatusValueDto(string Status, string Meaning);
|
||||||
|
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
||||||
|
|
||||||
public sealed record TaskDto(
|
public sealed record TaskDto(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -38,7 +39,7 @@ public sealed record TaskDto(
|
|||||||
|
|
||||||
public sealed record WorktreeInfoDto(
|
public sealed record WorktreeInfoDto(
|
||||||
string Path, string Branch, string HeadCommit, string BaseCommit,
|
string Path, string Branch, string HeadCommit, string BaseCommit,
|
||||||
int Ahead, int Behind, bool IsDirty);
|
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
|
||||||
|
|
||||||
public sealed record TaskDiffDto(
|
public sealed record TaskDiffDto(
|
||||||
string Content, IReadOnlyList<string> Files, bool Truncated, int TotalBytes);
|
string Content, IReadOnlyList<string> Files, bool Truncated, int TotalBytes);
|
||||||
@@ -51,6 +52,17 @@ public sealed record MergeContinuationResultDto(
|
|||||||
bool Merged, string TaskStatus, IReadOnlyList<string> Conflicts,
|
bool Merged, string TaskStatus, IReadOnlyList<string> Conflicts,
|
||||||
string? RepoPath, string? Message);
|
string? RepoPath, string? Message);
|
||||||
|
|
||||||
|
public sealed record MergePreviewToolDto(
|
||||||
|
string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind);
|
||||||
|
|
||||||
|
public sealed record MergePreviewSetEntryDto(
|
||||||
|
string TaskId, string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount, int Behind, string? Error);
|
||||||
|
|
||||||
|
public sealed record FileOverlapDto(string File, IReadOnlyList<string> TaskIds);
|
||||||
|
|
||||||
|
public sealed record MergePreviewSetResultDto(
|
||||||
|
IReadOnlyList<MergePreviewSetEntryDto> Tasks, IReadOnlyList<FileOverlapDto> Overlaps);
|
||||||
|
|
||||||
public sealed record WorktreeListItemDto(
|
public sealed record WorktreeListItemDto(
|
||||||
string? TaskId, string Path, string Branch,
|
string? TaskId, string Path, string Branch,
|
||||||
string HeadCommit, bool IsDirty, bool MergedIntoMain);
|
string HeadCommit, bool IsDirty, bool MergedIntoMain);
|
||||||
@@ -58,6 +70,9 @@ public sealed record WorktreeListItemDto(
|
|||||||
public sealed record CleanupWorktreeResult(
|
public sealed record CleanupWorktreeResult(
|
||||||
bool Removed, string WorktreePath, bool BranchDeleted);
|
bool Removed, string WorktreePath, bool BranchDeleted);
|
||||||
|
|
||||||
|
public sealed record RevertMergeResultDto(
|
||||||
|
bool Reverted, string? RevertCommit, IReadOnlyList<string> Conflicts, string? Message);
|
||||||
|
|
||||||
public sealed record DailyPrepCandidateDto(
|
public sealed record DailyPrepCandidateDto(
|
||||||
string Id, string ListId, string ListName, string Title, string? Description,
|
string Id, string ListId, string ListName, string Title, string? Description,
|
||||||
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
||||||
@@ -422,8 +437,8 @@ public sealed class ExternalMcpService
|
|||||||
mergeStatus, mergeConflicts, mergeMessage, repoPath);
|
mergeStatus, mergeConflicts, mergeMessage, repoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Immediately run a task in the override execution slot (bypasses the agent queue).")]
|
[McpServerTool, Description("Immediately run a task in the override execution slot (bypasses the agent queue). Returns { started: true, taskId } on success.")]
|
||||||
public async Task RunTaskNow(string taskId, CancellationToken cancellationToken)
|
public async Task<RunTaskNowResult> RunTaskNow(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -438,6 +453,7 @@ public sealed class ExternalMcpService
|
|||||||
throw new InvalidOperationException($"Task {taskId} not found.");
|
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
}
|
}
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return new RunTaskNowResult(true, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Cancel a running task. Returns { cancelled: true, id } if the task was running and cancellation was requested; cancelled is false if the task was not running.")]
|
[McpServerTool, Description("Cancel a running task. Returns { cancelled: true, id } if the task was running and cancellation was requested; cancelled is false if the task was not running.")]
|
||||||
@@ -484,7 +500,10 @@ public sealed class ExternalMcpService
|
|||||||
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
|
"Get git worktree details for a task: path, branch, headCommit (current HEAD SHA), " +
|
||||||
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
|
"baseCommit (SHA where the branch was created), ahead (commits on branch since base), " +
|
||||||
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
|
"behind (commits on main not yet on this branch; 0 if 'main' ref is unreachable), " +
|
||||||
"isDirty (has uncommitted changes in the worktree directory). " +
|
"isDirty (has uncommitted changes in the worktree directory), " +
|
||||||
|
"mergeCommit (SHA of the merge commit this worktree's branch produced on the target branch, " +
|
||||||
|
"if it has been merged and that succeeded after this field was introduced; null otherwise — " +
|
||||||
|
"required by revert_merge). " +
|
||||||
"Throws if the task or its worktree does not exist.")]
|
"Throws if the task or its worktree does not exist.")]
|
||||||
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
|
public async Task<WorktreeInfoDto> GetTaskWorktree(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -498,33 +517,34 @@ public sealed class ExternalMcpService
|
|||||||
var ahead = await GitRevListCountAsync(wt.Path, $"{wt.BaseCommit}..HEAD", cancellationToken);
|
var ahead = await GitRevListCountAsync(wt.Path, $"{wt.BaseCommit}..HEAD", cancellationToken);
|
||||||
var behind = await GitRevListCountAsync(wt.Path, "HEAD..main", cancellationToken);
|
var behind = await GitRevListCountAsync(wt.Path, "HEAD..main", cancellationToken);
|
||||||
|
|
||||||
return new WorktreeInfoDto(wt.Path, wt.BranchName, headCommit!, wt.BaseCommit, ahead, behind, isDirty);
|
return new WorktreeInfoDto(wt.Path, wt.BranchName, headCommit!, wt.BaseCommit, ahead, behind, isDirty, wt.MergeCommit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Get the diff for a task's worktree relative to its base commit. " +
|
"Get the diff for a task's worktree relative to its base commit. For a worktree-less " +
|
||||||
|
"list-handler host task (Mission Control's \"Let Claude handle it\"), returns the fixed " +
|
||||||
|
"HandlerBaseCommit..HandlerHeadCommit range over the list's working dir instead. " +
|
||||||
"stat=false (default): returns the full unified diff, capped at 200 KB (truncated=true when larger). " +
|
"stat=false (default): returns the full unified diff, capped at 200 KB (truncated=true when larger). " +
|
||||||
"stat=true: returns a --stat summary (changed files with insertion/deletion counts). " +
|
"stat=true: returns a --stat summary (changed files with insertion/deletion counts). " +
|
||||||
"files always lists the changed file paths regardless of stat mode. " +
|
"files always lists the changed file paths regardless of stat mode. " +
|
||||||
"totalBytes is the uncapped diff size (useful when truncated=true). " +
|
"totalBytes is the uncapped diff size (useful when truncated=true). " +
|
||||||
"Throws if the task has no worktree or the worktree directory is missing from disk.")]
|
"Throws if the task has no worktree/review range, or the relevant directory is missing from disk.")]
|
||||||
public async Task<TaskDiffDto> GetTaskDiff(
|
public async Task<TaskDiffDto> GetTaskDiff(
|
||||||
string taskId, bool stat = false, CancellationToken cancellationToken = default)
|
string taskId, bool stat = false, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var (_, _, wt) = await LoadWorktreeContextAsync(taskId, cancellationToken);
|
var (repoPath, baseCommit, headCommit) = await LoadDiffRangeAsync(taskId, cancellationToken);
|
||||||
|
|
||||||
if (!Directory.Exists(wt.Path))
|
|
||||||
throw new InvalidOperationException($"Worktree directory does not exist on disk: {wt.Path}");
|
|
||||||
|
|
||||||
const int maxBytes = 200 * 1024;
|
const int maxBytes = 200 * 1024;
|
||||||
|
|
||||||
if (stat)
|
if (stat)
|
||||||
{
|
{
|
||||||
var diffStat = await _git.DiffStatAsync(wt.Path, wt.BaseCommit, "HEAD", cancellationToken);
|
var diffStat = await _git.DiffStatAsync(repoPath, baseCommit, headCommit ?? "HEAD", cancellationToken);
|
||||||
return new TaskDiffDto(diffStat, ParseDiffStatFileNames(diffStat), false, diffStat.Length);
|
return new TaskDiffDto(diffStat, ParseDiffStatFileNames(diffStat), false, diffStat.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
var diff = await _git.GetBranchDiffAsync(wt.Path, wt.BaseCommit, cancellationToken);
|
var diff = headCommit is null
|
||||||
|
? await _git.GetBranchDiffAsync(repoPath, baseCommit, cancellationToken)
|
||||||
|
: await _git.GetCommitRangeDiffAsync(repoPath, baseCommit, headCommit, cancellationToken);
|
||||||
var files = ParseDiffFileNames(diff);
|
var files = ParseDiffFileNames(diff);
|
||||||
|
|
||||||
if (diff.Length <= maxBytes)
|
if (diff.Length <= maxBytes)
|
||||||
@@ -533,6 +553,37 @@ public sealed class ExternalMcpService
|
|||||||
return new TaskDiffDto(diff[..maxBytes], files, true, diff.Length);
|
return new TaskDiffDto(diff[..maxBytes], files, true, diff.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolves where a task's diff lives: a live worktree (repo path = worktree path, diffed
|
||||||
|
// against HEAD) or, for a worktree-less list-handler host task, the fixed
|
||||||
|
// HandlerBaseCommit..HandlerHeadCommit range over the list's working dir (headCommit
|
||||||
|
// non-null signals "fixed range" to the caller instead of "diff against live HEAD").
|
||||||
|
private async Task<(string RepoPath, string BaseCommit, string? HeadCommit)> LoadDiffRangeAsync(
|
||||||
|
string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||||
|
|
||||||
|
if (wt is not null)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(wt.Path))
|
||||||
|
throw new InvalidOperationException($"Worktree directory does not exist on disk: {wt.Path}");
|
||||||
|
return (wt.Path, wt.BaseCommit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.HandlerBaseCommit is { Length: > 0 } handlerBase && task.HandlerHeadCommit is { Length: > 0 } handlerHead)
|
||||||
|
{
|
||||||
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||||
|
?? throw new InvalidOperationException("List not found.");
|
||||||
|
if (string.IsNullOrEmpty(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
|
||||||
|
throw new InvalidOperationException("The list's working directory no longer exists.");
|
||||||
|
return (list.WorkingDir, handlerBase, handlerHead);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Task {taskId} has no worktree.");
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"Merge a task's worktree branch into targetBranch (default: main). " +
|
"Merge a task's worktree branch into targetBranch (default: main). " +
|
||||||
"noFf=true (default): always creates a merge commit (--no-ff). " +
|
"noFf=true (default): always creates a merge commit (--no-ff). " +
|
||||||
@@ -695,6 +746,132 @@ public sealed class ExternalMcpService
|
|||||||
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Non-destructive merge preview for a task's worktree branch against targetBranch (default: the repo's " +
|
||||||
|
"current branch), via `git merge-tree --write-tree` — does NOT touch the working tree, index, or HEAD. " +
|
||||||
|
"status: 'clean' (mergeable; changedFileCount is the size of that merge) or 'conflict' (conflictFiles " +
|
||||||
|
"lists the paths git would stop on). behind = commits on targetBranch not yet on this task's branch, so " +
|
||||||
|
"you can spot a stale branch even when the preview itself is clean. " +
|
||||||
|
"IMPORTANT: a clean preview says nothing about whether the merged result compiles or passes tests — git " +
|
||||||
|
"can merge two changes cleanly (e.g. one file deletes a symbol another file still references) and still " +
|
||||||
|
"break the build. " +
|
||||||
|
"Throws a clear error if the task has no worktree, the worktree is not Active, or the list's working " +
|
||||||
|
"directory is missing from disk.")]
|
||||||
|
public async Task<MergePreviewToolDto> PreviewMerge(
|
||||||
|
string taskId,
|
||||||
|
string? targetBranch = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var (preview, behind, _) = await PreviewMergeCoreAsync(taskId, targetBranch, cancellationToken);
|
||||||
|
return new MergePreviewToolDto(preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Merge preview plus file-overlap check across several tasks at once, all previewed against the same " +
|
||||||
|
"targetBranch (default: the repo's current branch). For each taskId returns the same fields as " +
|
||||||
|
"preview_merge (status/conflictFiles/changedFileCount/behind; error is set instead if that task could not " +
|
||||||
|
"be previewed, and it is then excluded from the overlap computation). overlaps lists, for each file " +
|
||||||
|
"touched by MORE THAN ONE of the given tasks (via each task's own diff, not the merge preview itself), " +
|
||||||
|
"which tasks touch it — passing a single taskId always yields an empty overlaps list. " +
|
||||||
|
"IMPORTANT: file-name overlap is a HINT, not a guarantee of a real collision, and its absence is not a " +
|
||||||
|
"guarantee of safety — two tasks touching different files entirely (e.g. one deletes a symbol, another " +
|
||||||
|
"still references it elsewhere) can still collide, and this tool will not flag that case.")]
|
||||||
|
public async Task<MergePreviewSetResultDto> PreviewMergeSet(
|
||||||
|
IReadOnlyList<string> taskIds,
|
||||||
|
string? targetBranch = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (taskIds is null || taskIds.Count == 0)
|
||||||
|
throw new InvalidOperationException("taskIds must contain at least one task id.");
|
||||||
|
|
||||||
|
var entries = new List<MergePreviewSetEntryDto>();
|
||||||
|
var filesByTask = new Dictionary<string, IReadOnlyList<string>>();
|
||||||
|
|
||||||
|
foreach (var taskId in taskIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (preview, behind, changedFiles) = await PreviewMergeCoreAsync(taskId, targetBranch, cancellationToken);
|
||||||
|
entries.Add(new MergePreviewSetEntryDto(
|
||||||
|
taskId, preview.Status, preview.ConflictFiles, preview.ChangedFileCount, behind, null));
|
||||||
|
filesByTask[taskId] = changedFiles;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
entries.Add(new MergePreviewSetEntryDto(
|
||||||
|
taskId, TaskMergeService.PreviewUnavailable, Array.Empty<string>(), 0, 0, ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var overlaps = filesByTask
|
||||||
|
.SelectMany(kv => kv.Value.Select(f => (File: f, TaskId: kv.Key)))
|
||||||
|
.GroupBy(x => x.File, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(g => g.Select(x => x.TaskId).Distinct().Count() > 1)
|
||||||
|
.Select(g => new FileOverlapDto(g.Key, g.Select(x => x.TaskId).Distinct().ToList()))
|
||||||
|
.OrderBy(o => o.File, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new MergePreviewSetResultDto(entries, overlaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared core for PreviewMerge/PreviewMergeSet: throws a clear InvalidOperationException instead of
|
||||||
|
// TaskMergeService.PreviewAsync's silent "unavailable" status, and adds `behind` + the task's own
|
||||||
|
// changed-file list (via diff-stat, not the merge-tree preview) for overlap detection.
|
||||||
|
private async Task<(MergePreviewResult Preview, int Behind, IReadOnlyList<string> ChangedFiles)> PreviewMergeCoreAsync(
|
||||||
|
string taskId, string? targetBranch, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var (_, list, wt) = await LoadWorktreeContextAsync(taskId, ct);
|
||||||
|
if (wt.State != WorktreeState.Active)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Worktree state must be Active to preview a merge (current: {wt.State}).");
|
||||||
|
if (string.IsNullOrWhiteSpace(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
|
||||||
|
throw new InvalidOperationException("The list's working directory no longer exists.");
|
||||||
|
|
||||||
|
var preview = await _merge.PreviewAsync(taskId, targetBranch ?? "", ct);
|
||||||
|
if (preview.Status == TaskMergeService.PreviewUnavailable)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Merge preview unavailable for this task (worktree inactive or repo is not a git repository).");
|
||||||
|
|
||||||
|
var target = string.IsNullOrWhiteSpace(targetBranch)
|
||||||
|
? await _git.GetCurrentBranchAsync(list.WorkingDir, ct)
|
||||||
|
: targetBranch;
|
||||||
|
var behind = await GitRevListCountAsync(list.WorkingDir, $"{wt.BranchName}..{target}", ct);
|
||||||
|
|
||||||
|
var changedFiles = Directory.Exists(wt.Path)
|
||||||
|
? ParseDiffStatFileNames(await _git.DiffStatAsync(wt.Path, wt.BaseCommit, "HEAD", ct))
|
||||||
|
: Array.Empty<string>();
|
||||||
|
|
||||||
|
return (preview, behind, changedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Revert a previously merged task's merge commit on targetBranch (default: main), via `git revert -m 1` — " +
|
||||||
|
"a new commit, never a reset/rewrite (the target working directory is shared with other sessions). " +
|
||||||
|
"Requires the task to be Done with a Merged worktree that has a recorded merge commit; tasks merged " +
|
||||||
|
"before this feature existed have no recorded commit and are refused rather than guessed via git log. " +
|
||||||
|
"On success: reverted=true, revertCommit is the new commit's SHA, and the task returns to " +
|
||||||
|
"WaitingForReview so it can be reconsidered. " +
|
||||||
|
"On a conflicting revert: reverted=false, the revert is aborted immediately (no half-resolved state " +
|
||||||
|
"left in the tree) and conflicts lists the files that would have conflicted. " +
|
||||||
|
"Throws if there is no recorded merge commit, the repo is mid-merge/mid-revert, or the target working " +
|
||||||
|
"tree has uncommitted changes from another session.")]
|
||||||
|
public async Task<RevertMergeResultDto> RevertMerge(
|
||||||
|
string taskId, string targetBranch = "main", CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await _merge.RevertMergeAsync(taskId, targetBranch, cancellationToken);
|
||||||
|
|
||||||
|
if (result.Status == TaskMergeService.StatusReverted)
|
||||||
|
{
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return new RevertMergeResultDto(true, result.RevertCommit, Array.Empty<string>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Status == TaskMergeService.StatusConflictAborted)
|
||||||
|
return new RevertMergeResultDto(false, null, result.ConflictFiles, result.ErrorMessage);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(result.ErrorMessage ?? $"Revert blocked: {result.Status}");
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description(
|
[McpServerTool, Description(
|
||||||
"List all ClaudeDo-tracked worktrees. " +
|
"List all ClaudeDo-tracked worktrees. " +
|
||||||
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +
|
"Each entry: taskId, path, branch, headCommit (empty if path missing on disk), " +
|
||||||
|
|||||||
+5
-2
@@ -6,6 +6,8 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|||||||
|
|
||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
|
public sealed record ResetFailedTaskResult(bool Reset, string TaskId);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class LifecycleMcpTools
|
public sealed class LifecycleMcpTools
|
||||||
{
|
{
|
||||||
@@ -18,8 +20,8 @@ public sealed class LifecycleMcpTools
|
|||||||
_reset = reset;
|
_reset = reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Reset a failed task: discards its worktree and returns it to Idle so it can be run again. Only Failed tasks are accepted.")]
|
[McpServerTool, Description("Reset a failed task: discards its worktree and returns it to Idle so it can be run again. Only Failed tasks are accepted. Returns { reset: true, taskId } on success.")]
|
||||||
public async Task ResetFailedTask(string taskId, CancellationToken cancellationToken)
|
public async Task<ResetFailedTaskResult> ResetFailedTask(string taskId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
@@ -27,5 +29,6 @@ public sealed class LifecycleMcpTools
|
|||||||
throw new InvalidOperationException($"Task {taskId} is {task.Status}, not Failed. Only failed tasks can be reset via this tool.");
|
throw new InvalidOperationException($"Task {taskId} is {task.Status}, not Failed. Only failed tasks can be reset via this tool.");
|
||||||
|
|
||||||
await _reset.ResetAsync(taskId, cancellationToken);
|
await _reset.ResetAsync(taskId, cancellationToken);
|
||||||
|
return new ResetFailedTaskResult(true, taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -7,6 +7,7 @@ using ModelContextProtocol.Server;
|
|||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
public sealed record ListSummaryDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public sealed record ListSummaryDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
|
public sealed record DeleteListResult(bool Deleted, string Id);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class ListMcpTools
|
public sealed class ListMcpTools
|
||||||
@@ -61,13 +62,14 @@ public sealed class ListMcpTools
|
|||||||
return ToDto(entity);
|
return ToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Delete a list and its tasks. Irreversible.")]
|
[McpServerTool, Description("Delete a list and its tasks. Irreversible. Returns { deleted: true, id } on success.")]
|
||||||
public async Task DeleteList(string listId, CancellationToken cancellationToken)
|
public async Task<DeleteListResult> DeleteList(string listId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"List {listId} not found.");
|
?? throw new InvalidOperationException($"List {listId} not found.");
|
||||||
await _lists.DeleteAsync(listId, cancellationToken);
|
await _lists.DeleteAsync(listId, cancellationToken);
|
||||||
await _broadcaster.ListUpdated(listId);
|
await _broadcaster.ListUpdated(listId);
|
||||||
|
return new DeleteListResult(true, listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ListSummaryDto ToDto(ListEntity l) =>
|
private static ListSummaryDto ToDto(ListEntity l) =>
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
|
public sealed record TaskStatusChangeDto(string TaskId, string Status);
|
||||||
|
public sealed record WaitForTaskChangeResult(IReadOnlyList<TaskStatusChangeDto> Changed, bool TimedOut);
|
||||||
|
|
||||||
|
[McpServerToolType]
|
||||||
|
public sealed class TaskWaitMcpTools
|
||||||
|
{
|
||||||
|
// InteractiveLaunchSpecService sets MCP_TOOL_TIMEOUT=200000ms for the list handler
|
||||||
|
// session; this cap leaves a ~30s margin so the tool itself reports TimedOut instead
|
||||||
|
// of racing the client's own abort.
|
||||||
|
internal const int MaxTimeoutSeconds = 170;
|
||||||
|
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);
|
||||||
|
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
|
||||||
|
public TaskWaitMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Blocks until at least one of the given tasks leaves Queued/Running, or until timeoutSeconds elapses " +
|
||||||
|
"(clamped server-side to 170s). Returns immediately if any task is already outside Queued/Running " +
|
||||||
|
"when called (an unknown id is reported as status \"NotFound\" and counts as changed). Use this instead " +
|
||||||
|
"of polling get_task in a loop. Result: { changed: [{ taskId, status }], timedOut }.")]
|
||||||
|
public async Task<WaitForTaskChangeResult> WaitForTaskChange(
|
||||||
|
string[] taskIds, int timeoutSeconds = 60, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (taskIds.Length == 0)
|
||||||
|
throw new ArgumentException("taskIds must not be empty.", nameof(taskIds));
|
||||||
|
|
||||||
|
var timeout = TimeSpan.FromSeconds(Math.Clamp(timeoutSeconds, 1, MaxTimeoutSeconds));
|
||||||
|
using var timeoutCts = new CancellationTokenSource(timeout);
|
||||||
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var changed = await CheckOnceAsync(taskIds, linked.Token);
|
||||||
|
if (changed.Count > 0)
|
||||||
|
return new WaitForTaskChangeResult(changed, TimedOut: false);
|
||||||
|
|
||||||
|
await Task.Delay(PollInterval, linked.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new WaitForTaskChangeResult(Array.Empty<TaskStatusChangeDto>(), TimedOut: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<TaskStatusChangeDto>> CheckOnceAsync(string[] taskIds, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var rows = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => taskIds.Contains(t.Id))
|
||||||
|
.Select(t => new { t.Id, t.Status })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var byId = rows.ToDictionary(r => r.Id, r => r.Status);
|
||||||
|
var result = new List<TaskStatusChangeDto>();
|
||||||
|
foreach (var id in taskIds)
|
||||||
|
{
|
||||||
|
if (!byId.TryGetValue(id, out var status))
|
||||||
|
{
|
||||||
|
result.Add(new TaskStatusChangeDto(id, "NotFound"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status != TaskStatus.Queued && status != TaskStatus.Running)
|
||||||
|
result.Add(new TaskStatusChangeDto(id, status.ToString()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,9 @@ public sealed class HubBroadcaster : IPrimeBroadcaster, IRefineBroadcaster
|
|||||||
public Task RunCreated(string taskId, int runNumber, bool isRetry) =>
|
public Task RunCreated(string taskId, int runNumber, bool isRetry) =>
|
||||||
_hub.Clients.All.SendAsync("RunCreated", taskId, runNumber, isRetry);
|
_hub.Clients.All.SendAsync("RunCreated", taskId, runNumber, isRetry);
|
||||||
|
|
||||||
|
public Task UsageUpdated(UsageSnapshotDto snapshot) =>
|
||||||
|
_hub.Clients.All.SendAsync("UsageUpdated", snapshot);
|
||||||
|
|
||||||
public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) =>
|
public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) =>
|
||||||
_hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc);
|
_hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ using ClaudeDo.Worker.Report;
|
|||||||
using ClaudeDo.Worker.Report.Interfaces;
|
using ClaudeDo.Worker.Report.Interfaces;
|
||||||
using ClaudeDo.Worker.Skills;
|
using ClaudeDo.Worker.Skills;
|
||||||
using ClaudeDo.Worker.State;
|
using ClaudeDo.Worker.State;
|
||||||
|
using ClaudeDo.Worker.Usage;
|
||||||
|
using ClaudeDo.Worker.Usage.Interfaces;
|
||||||
using ClaudeDo.Worker.Worktrees;
|
using ClaudeDo.Worker.Worktrees;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
@@ -41,7 +43,9 @@ public record AppSettingsDto(
|
|||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks,
|
int DailyPrepMaxTasks,
|
||||||
List<string>? SessionSkills = null,
|
List<string>? SessionSkills = null,
|
||||||
List<ModelPresetDto>? ModelPresets = null);
|
List<ModelPresetDto>? ModelPresets = null,
|
||||||
|
int UsageGateFiveHourPct = 80,
|
||||||
|
int UsageGateSevenDayPct = 90);
|
||||||
|
|
||||||
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
|
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
|
||||||
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||||
@@ -79,9 +83,9 @@ public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDoc
|
|||||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||||
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false);
|
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false);
|
||||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
||||||
public record SeedResultDto(int Copied, int Skipped);
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public record OnlineInboxStateDto(
|
public record OnlineInboxStateDto(
|
||||||
@@ -103,6 +107,49 @@ public record OnlineInboxConfigInput(
|
|||||||
string Scopes,
|
string Scopes,
|
||||||
string RedirectUri);
|
string RedirectUri);
|
||||||
|
|
||||||
|
public record UsageLimitDto(
|
||||||
|
string Kind,
|
||||||
|
string Group,
|
||||||
|
double Percent,
|
||||||
|
string Severity,
|
||||||
|
DateTimeOffset? ResetsAt,
|
||||||
|
string? ScopeModelDisplayName,
|
||||||
|
bool IsActive);
|
||||||
|
|
||||||
|
public record UsageSnapshotDto(
|
||||||
|
double? FiveHourPercent,
|
||||||
|
DateTimeOffset? FiveHourResetsAt,
|
||||||
|
double? SevenDayPercent,
|
||||||
|
DateTimeOffset? SevenDayResetsAt,
|
||||||
|
IReadOnlyList<UsageLimitDto> Limits,
|
||||||
|
int FiveHourThresholdPct,
|
||||||
|
int SevenDayThresholdPct,
|
||||||
|
bool IsGateBlocked,
|
||||||
|
string? GateReason,
|
||||||
|
DateTime? FetchedAtUtc,
|
||||||
|
bool IsStale,
|
||||||
|
string? LastError);
|
||||||
|
|
||||||
|
public record ModelUsageRowDto(
|
||||||
|
DateOnly Date,
|
||||||
|
string Model,
|
||||||
|
string Scope,
|
||||||
|
long InputTokens,
|
||||||
|
long OutputTokens,
|
||||||
|
long CacheReadTokens,
|
||||||
|
long CacheCreationTokens,
|
||||||
|
int Messages);
|
||||||
|
|
||||||
|
public record TaskUsageRowDto(
|
||||||
|
string TaskId,
|
||||||
|
string TaskTitle,
|
||||||
|
string ListId,
|
||||||
|
string ListName,
|
||||||
|
string? Model,
|
||||||
|
int Runs,
|
||||||
|
long TokensIn,
|
||||||
|
long TokensOut);
|
||||||
|
|
||||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||||
{
|
{
|
||||||
private static readonly string Version =
|
private static readonly string Version =
|
||||||
@@ -135,6 +182,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly ISessionSkillRegistry _skillRegistry;
|
private readonly ISessionSkillRegistry _skillRegistry;
|
||||||
private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec;
|
private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec;
|
||||||
private readonly WorktreeManager? _worktreeManager;
|
private readonly WorktreeManager? _worktreeManager;
|
||||||
|
private readonly Data.Git.GitService? _git;
|
||||||
|
private readonly UsageSnapshotBuilder? _usageSnapshotBuilder;
|
||||||
|
private readonly ITranscriptUsageReader? _usageReader;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -163,7 +213,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
ISessionSkillRegistry skillRegistry,
|
ISessionSkillRegistry skillRegistry,
|
||||||
LogRingBuffer? logBuffer = null,
|
LogRingBuffer? logBuffer = null,
|
||||||
IInteractiveLaunchSpecService? interactiveLaunchSpec = null,
|
IInteractiveLaunchSpecService? interactiveLaunchSpec = null,
|
||||||
WorktreeManager? worktreeManager = null)
|
WorktreeManager? worktreeManager = null,
|
||||||
|
Data.Git.GitService? git = null,
|
||||||
|
UsageSnapshotBuilder? usageSnapshotBuilder = null,
|
||||||
|
ITranscriptUsageReader? usageReader = null)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_waker = waker;
|
_waker = waker;
|
||||||
@@ -192,6 +245,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_logBuffer = logBuffer;
|
_logBuffer = logBuffer;
|
||||||
_interactiveLaunchSpec = interactiveLaunchSpec;
|
_interactiveLaunchSpec = interactiveLaunchSpec;
|
||||||
_worktreeManager = worktreeManager;
|
_worktreeManager = worktreeManager;
|
||||||
|
_git = git;
|
||||||
|
_usageSnapshotBuilder = usageSnapshotBuilder;
|
||||||
|
_usageReader = usageReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
||||||
@@ -324,7 +380,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.DailyPrepMaxTasks,
|
row.DailyPrepMaxTasks,
|
||||||
SkillsFromJson(row.SessionSkills),
|
SkillsFromJson(row.SessionSkills),
|
||||||
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
||||||
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList());
|
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList(),
|
||||||
|
row.UsageGateFiveHourPct,
|
||||||
|
row.UsageGateSevenDayPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
@@ -352,6 +410,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
? Data.Models.ModelPresets.Serialize(
|
? Data.Models.ModelPresets.Serialize(
|
||||||
dto.ModelPresets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)))
|
dto.ModelPresets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)))
|
||||||
: Data.Models.ModelPresets.SerializeDefaults(),
|
: Data.Models.ModelPresets.SerializeDefaults(),
|
||||||
|
UsageGateFiveHourPct = dto.UsageGateFiveHourPct,
|
||||||
|
UsageGateSevenDayPct = dto.UsageGateSevenDayPct,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,8 +578,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
||||||
var agentPath = dto.AgentPath.NullIfBlank();
|
var agentPath = dto.AgentPath.NullIfBlank();
|
||||||
var sessionSkills = SkillsToJson(dto.SessionSkills);
|
var sessionSkills = SkillsToJson(dto.SessionSkills);
|
||||||
|
var verifyCommand = dto.VerifyCommand.NullIfBlank();
|
||||||
|
|
||||||
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null)
|
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null)
|
||||||
{
|
{
|
||||||
await repo.DeleteConfigAsync(dto.ListId);
|
await repo.DeleteConfigAsync(dto.ListId);
|
||||||
}
|
}
|
||||||
@@ -533,6 +594,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
AgentPath = agentPath,
|
AgentPath = agentPath,
|
||||||
MaxTurns = dto.MaxTurns,
|
MaxTurns = dto.MaxTurns,
|
||||||
SessionSkills = sessionSkills,
|
SessionSkills = sessionSkills,
|
||||||
|
VerifyCommand = verifyCommand,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +607,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var repo = new ListRepository(ctx);
|
var repo = new ListRepository(ctx);
|
||||||
var config = await repo.GetConfigAsync(listId);
|
var config = await repo.GetConfigAsync(listId);
|
||||||
if (config is null) return null;
|
if (config is null) return null;
|
||||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills));
|
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTaskStatus(string taskId, string status)
|
public async Task SetTaskStatus(string taskId, string status)
|
||||||
@@ -636,42 +698,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picks up a task's Claude session in a real terminal window (--resume) so the user can
|
|
||||||
// drive it by hand. Only for tasks the worker isn't actively running, with a persisted
|
|
||||||
// session id and a live worktree.
|
|
||||||
public Task ResumeTaskInTerminal(string taskId) => HubGuard(async () =>
|
|
||||||
{
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
||||||
|
|
||||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, Context.ConnectionAborted)
|
|
||||||
?? throw new KeyNotFoundException();
|
|
||||||
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
|
||||||
throw new InvalidOperationException("Can't pick up a running or queued task — interrupt it first.");
|
|
||||||
|
|
||||||
var run = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, Context.ConnectionAborted);
|
|
||||||
if (run?.SessionId is not { Length: > 0 } sessionId)
|
|
||||||
throw new InvalidOperationException("This task has no resumable Claude session yet.");
|
|
||||||
|
|
||||||
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
|
||||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
|
||||||
throw new InvalidOperationException("This task has no active worktree to resume in.");
|
|
||||||
if (!Directory.Exists(worktree.Path))
|
|
||||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _launcher.LaunchResumeAsync(worktree.Path, sessionId, Context.ConnectionAborted);
|
|
||||||
}
|
|
||||||
catch (TerminalLaunchException ex)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(ex.Message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Builds the launch spec an embedded ConPTY terminal (UI process) needs to open an
|
// Builds the launch spec an embedded ConPTY terminal (UI process) needs to open an
|
||||||
// interactive Claude session in a task's worktree -- same worktree prep as an
|
// interactive Claude session in a task's worktree -- same worktree prep as an
|
||||||
// autonomous run (session-skills seeding, run env vars), --resume if the task has a
|
// autonomous run (session-skills seeding, run env vars), --resume if the task has a
|
||||||
// persisted session or a fresh-start spec otherwise. Guards mirror ResumeTaskInTerminal.
|
// persisted session or a fresh-start spec otherwise. Guards: no running/queued task,
|
||||||
|
// and (once a worktree exists) it must be live on disk.
|
||||||
public Task<LaunchSpec> GetInteractiveLaunchSpec(string taskId) => HubGuard(() =>
|
public Task<LaunchSpec> GetInteractiveLaunchSpec(string taskId) => HubGuard(() =>
|
||||||
{
|
{
|
||||||
if (_interactiveLaunchSpec is null)
|
if (_interactiveLaunchSpec is null)
|
||||||
@@ -698,6 +729,19 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted);
|
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
||||||
|
// one task per run, never queued (Idle/IsManual). Broadcasts TaskUpdated so it shows up in
|
||||||
|
// the list immediately.
|
||||||
|
public Task<string> CreateMergeHelperTask(string[] taskIds, string listId, string title, string descriptionHeader) => HubGuard(async () =>
|
||||||
|
{
|
||||||
|
if (_interactiveLaunchSpec is null)
|
||||||
|
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
||||||
|
var taskId = await _interactiveLaunchSpec.CreateMergeHelperTaskAsync(
|
||||||
|
taskIds, listId, title, descriptionHeader, Context.ConnectionAborted);
|
||||||
|
await Clients.All.SendAsync("TaskUpdated", taskId);
|
||||||
|
return taskId;
|
||||||
|
});
|
||||||
|
|
||||||
// Starts a planning session (worktree + prompt files + token, task -> Planning) and returns
|
// Starts a planning session (worktree + prompt files + token, task -> Planning) and returns
|
||||||
// the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for
|
// the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for
|
||||||
// StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started
|
// StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started
|
||||||
@@ -730,17 +774,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
return _interactiveLaunchSpec.BuildPlanningResume(ctx);
|
return _interactiveLaunchSpec.BuildPlanningResume(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Submits an interactively-worked task for review: commits whatever the ConPTY session left
|
// Submits an interactively-worked task for review, then transitions Idle/Failed ->
|
||||||
// in the worktree (so there is a diff to merge), then transitions Idle/Failed -> WaitingForReview.
|
// WaitingForReview. The normal Approve flow then merges it (or, for a worktree-less host
|
||||||
// The normal Approve flow then merges it. This is the only path that flips a hand-driven session
|
// task below, just flips to Done — there's nothing to merge). This is the only path that
|
||||||
// into the review pipeline — a ConPTY session never touches task status on its own.
|
// flips a hand-driven session into the review pipeline — a ConPTY session never touches
|
||||||
|
// task status on its own.
|
||||||
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
public Task SubmitTaskForReview(string taskId) => HubGuard(async () =>
|
||||||
{
|
{
|
||||||
if (_worktreeManager is null)
|
|
||||||
throw new InvalidOperationException("Worktree manager is not configured.");
|
|
||||||
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, Context.ConnectionAborted)
|
var taskRepo = new TaskRepository(ctx);
|
||||||
|
var task = await taskRepo.GetByIdAsync(taskId, Context.ConnectionAborted)
|
||||||
?? throw new KeyNotFoundException();
|
?? throw new KeyNotFoundException();
|
||||||
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
if (task.Status is TaskStatus.Running or TaskStatus.Queued)
|
||||||
throw new InvalidOperationException("Can't submit a running or queued task — interrupt it first.");
|
throw new InvalidOperationException("Can't submit a running or queued task — interrupt it first.");
|
||||||
@@ -748,16 +791,41 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
throw new InvalidOperationException("Task is already awaiting review.");
|
throw new InvalidOperationException("Task is already awaiting review.");
|
||||||
|
|
||||||
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
var worktree = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, Context.ConnectionAborted);
|
||||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
if (worktree is not null)
|
||||||
|
{
|
||||||
|
if (_worktreeManager is null)
|
||||||
|
throw new InvalidOperationException("Worktree manager is not configured.");
|
||||||
|
if (worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||||
|
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||||
|
if (!Directory.Exists(worktree.Path))
|
||||||
|
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
||||||
|
|
||||||
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||||
|
?? throw new InvalidOperationException("Task list not found.");
|
||||||
|
|
||||||
|
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
||||||
|
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
|
||||||
|
}
|
||||||
|
else if (task.HandlerBaseCommit is { Length: > 0 })
|
||||||
|
{
|
||||||
|
// Worktree-less "list handler" host task (Mission Control's "Let Claude handle it"):
|
||||||
|
// the handler commits its own changes straight to the list's working dir, so there
|
||||||
|
// is nothing for us to commit here — just stamp the review range's head commit.
|
||||||
|
if (_git is null)
|
||||||
|
throw new InvalidOperationException("Git service is not configured.");
|
||||||
|
|
||||||
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
||||||
|
?? throw new InvalidOperationException("Task list not found.");
|
||||||
|
if (string.IsNullOrEmpty(list.WorkingDir) || !Directory.Exists(list.WorkingDir))
|
||||||
|
throw new InvalidOperationException("The list's working directory no longer exists.");
|
||||||
|
|
||||||
|
var headCommit = await _git.RevParseHeadAsync(list.WorkingDir, Context.ConnectionAborted);
|
||||||
|
await taskRepo.SetHandlerHeadCommitAsync(taskId, headCommit, Context.ConnectionAborted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
throw new InvalidOperationException("This task has no active worktree to submit.");
|
throw new InvalidOperationException("This task has no active worktree to submit.");
|
||||||
if (!Directory.Exists(worktree.Path))
|
}
|
||||||
throw new InvalidOperationException("The task's worktree directory no longer exists.");
|
|
||||||
|
|
||||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, Context.ConnectionAborted)
|
|
||||||
?? throw new InvalidOperationException("Task list not found.");
|
|
||||||
|
|
||||||
var wtCtx = new WorktreeContext(worktree.Path, worktree.BranchName, worktree.BaseCommit);
|
|
||||||
await _worktreeManager.CommitIfChangedAsync(wtCtx, task, list, Context.ConnectionAborted);
|
|
||||||
|
|
||||||
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, Context.ConnectionAborted);
|
var result = await _state.SubmitInteractiveForReviewAsync(taskId, DateTime.UtcNow, Context.ConnectionAborted);
|
||||||
if (!result.Ok)
|
if (!result.Ok)
|
||||||
@@ -967,4 +1035,65 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_onlineTokenStore.Clear();
|
_onlineTokenStore.Clear();
|
||||||
}
|
}
|
||||||
#pragma warning restore CA1416
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
|
public Task<UsageSnapshotDto> GetUsageSnapshot() => HubGuard(() =>
|
||||||
|
{
|
||||||
|
if (_usageSnapshotBuilder is null)
|
||||||
|
throw new InvalidOperationException("Usage snapshot builder is not configured.");
|
||||||
|
return _usageSnapshotBuilder.BuildAsync(Context.ConnectionAborted);
|
||||||
|
});
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsage(DateOnly from, DateOnly to) => HubGuard(async () =>
|
||||||
|
{
|
||||||
|
if (_usageReader is null)
|
||||||
|
throw new InvalidOperationException("Transcript usage reader is not configured.");
|
||||||
|
var rows = await _usageReader.ReadAsync(from, to, Context.ConnectionAborted);
|
||||||
|
return (IReadOnlyList<ModelUsageRowDto>)rows
|
||||||
|
.Select(r => new ModelUsageRowDto(
|
||||||
|
r.Date, r.Model, r.Scope == UsageScope.ClaudeDo ? "claudedo" : "other",
|
||||||
|
r.InputTokens, r.OutputTokens, r.CacheReadTokens, r.CacheCreationTokens, r.Messages))
|
||||||
|
.ToList();
|
||||||
|
});
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsage(DateOnly from, DateOnly to)
|
||||||
|
{
|
||||||
|
var fromDt = from.ToDateTime(TimeOnly.MinValue);
|
||||||
|
var toDt = to.ToDateTime(TimeOnly.MaxValue);
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(Context.ConnectionAborted);
|
||||||
|
var runs = await ctx.TaskRuns
|
||||||
|
.Where(r => r.StartedAt != null && r.StartedAt >= fromDt && r.StartedAt <= toDt)
|
||||||
|
.ToListAsync(Context.ConnectionAborted);
|
||||||
|
|
||||||
|
if (runs.Count == 0) return Array.Empty<TaskUsageRowDto>();
|
||||||
|
|
||||||
|
var taskIds = runs.Select(r => r.TaskId).Distinct().ToList();
|
||||||
|
var tasks = await ctx.Tasks.Where(t => taskIds.Contains(t.Id)).ToListAsync(Context.ConnectionAborted);
|
||||||
|
var taskById = tasks.ToDictionary(t => t.Id);
|
||||||
|
|
||||||
|
var listIds = tasks.Select(t => t.ListId).Distinct().ToList();
|
||||||
|
var lists = await ctx.Lists.Where(l => listIds.Contains(l.Id)).ToDictionaryAsync(l => l.Id, Context.ConnectionAborted);
|
||||||
|
|
||||||
|
return runs
|
||||||
|
.Where(r => taskById.ContainsKey(r.TaskId))
|
||||||
|
.GroupBy(r => r.TaskId)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
var task = taskById[g.Key];
|
||||||
|
var listName = lists.TryGetValue(task.ListId, out var list) ? list.Name : "";
|
||||||
|
var latestModel = g.OrderByDescending(r => r.StartedAt).First().Model;
|
||||||
|
return new TaskUsageRowDto(
|
||||||
|
task.Id,
|
||||||
|
task.Title,
|
||||||
|
task.ListId,
|
||||||
|
listName,
|
||||||
|
latestModel,
|
||||||
|
g.Count(),
|
||||||
|
g.Sum(r => (long)(r.TokensIn ?? 0)),
|
||||||
|
g.Sum(r => (long)(r.TokensOut ?? 0)));
|
||||||
|
})
|
||||||
|
.OrderByDescending(r => r.TokensIn + r.TokensOut)
|
||||||
|
.Take(100)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user