45 Commits

Author SHA1 Message Date
mika kuns
312b411654 i18n(de): add complete German translation
Full de.json mirroring en.json key-for-key (app + installer + VM strings);
enables Deutsch in the language switcher with live switching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:14:23 +02:00
mika kuns
364a037cb3 feat(i18n): localize installer with language picker and config write-through
- Init Localizer at app startup (before self-update prompt) and assign to TrExtension.Localizer
- Register ILocalizer in DI; inject into WizardViewModel and SettingsViewModel
- WizardViewModel: SelectedLanguage ComboBox binding with OnSelectedLanguageChanged -> SetLanguage + InstallContext.Language
- WizardWindow.xaml: DockPanel wraps step chips + language ComboBox (right-aligned)
- Localize all installer XAML: WizardWindow, WelcomePage, PathsPage, ServicePage, UiSettingsPage, InstallPage, SettingsWindow, SelfUpdatePromptWindow
- Localize page Title properties and WizardViewModel.NextButtonText via TrExtension.Localizer
- Persist chosen Language in WriteConfigStep and SettingsViewModel.Save into ui.config.json
- Append installer section to en.json (nav, welcome, paths, service, uiSettings, install, settings, selfUpdate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:08 +02:00
mika kuns
2fbf054a57 feat(i18n): add WPF localization primitives and Language config to installer 2026-06-03 12:45:49 +02:00
mika kuns
350a89f364 feat(i18n): localize ViewModel-built strings via ambient Loc accessor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:43:30 +02:00
mika kuns
086c6f6c45 feat(i18n): localize Avalonia view strings via loc:Tr markup
Extract ~165 hardcoded UI strings across islands, modals, planning and
shell views into en.json; replace with {loc:Tr} bindings.
2026-06-03 12:05:08 +02:00
mika kuns
070f5de1b1 feat(i18n): add language dropdown to settings and persist selection 2026-06-03 11:51:36 +02:00
mika kuns
f529a5ff22 feat(i18n): initialize Localizer at app startup from config/OS culture 2026-06-03 11:46:33 +02:00
mika kuns
6a85d82fcf feat(i18n): add Language preference and Save() to AppSettings 2026-06-03 11:45:06 +02:00
mika kuns
35ad1715d3 feat(i18n): add Avalonia loc:Tr markup extension and LocalizedString 2026-06-03 11:44:16 +02:00
mika kuns
3c40bb5ea3 feat(i18n): seed en.json and wire locale copy to app output 2026-06-03 11:41:51 +02:00
mika kuns
d95d55e6b8 feat(i18n): add CultureResolver for OS-culture mapping 2026-06-03 11:39:20 +02:00
mika kuns
d22b50e171 feat(i18n): add Localizer with fallback chain and change event 2026-06-03 11:38:49 +02:00
mika kuns
a83a0c41e8 feat(i18n): add LocaleStore folder discovery 2026-06-03 11:38:02 +02:00
mika kuns
9efde2bf88 feat(i18n): add ClaudeDo.Localization project with nested-JSON locale parser 2026-06-03 11:35:59 +02:00
mika kuns
8dc8b8ba8e docs: localization implementation plan
Phased TDD plan: shared ClaudeDo.Localization lib, Avalonia + WPF markup
extensions, settings/installer pickers, parallel string-extraction batches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:32:06 +02:00
mika kuns
baeea9c2a7 docs: localization (i18n) design spec
Live-switching, JSON locale files, shared ClaudeDo.Localization project,
English-only at launch with data-driven extensibility, installer parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:19:03 +02:00
mika kuns
a935bf9664 i18n(ui): English UI labels for weekly report and notes (report body stays German) 2026-06-03 10:44:36 +02:00
mika kuns
2d55f88a41 fix(ui): notes add row stays visible, English 'Add' label, Enter to add 2026-06-03 10:39:53 +02:00
mika kuns
a8d8a8bd65 fix(worker): sanitize report model arg, fix multi-repo summary attribution and standup-weekday sentinel 2026-06-03 10:22:06 +02:00
mika kuns
0bc3d2a6c4 docs: document weekly report and daily notes feature 2026-06-03 10:15:40 +02:00
mika kuns
b886d58c07 test: update fakes for new IWorkerClient members and WorkerHub/DetailsIslandViewModel ctor args 2026-06-03 10:13:56 +02:00
mika kuns
a8943a9f7a feat(ui): pinned Notes row in My Day opens the notes editor
Add ShowNotesRow/OpenNotesCommand to TasksIslandViewModel; wire NotesRequested
event to Details.ShowNotes() in the shell; show a Notes button above the task
list when the My Day smart list is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:08:30 +02:00
mika kuns
eccd06e182 feat(ui): notes mode in the Details island
Add IsNotesMode/Notes to DetailsIslandViewModel; ShowNotes() loads today's
notes and switches the island body to NotesEditorView via IsVisible toggling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:07:09 +02:00
mika kuns
731c291d61 feat(ui): NotesEditorView 2026-06-03 10:02:16 +02:00
mika kuns
c8b5ed3912 feat(ui): NotesEditorViewModel with day navigation and bullet CRUD 2026-06-03 10:01:17 +02:00
mika kuns
9bf44da13b feat(ui): INotesApi wrapper for daily notes 2026-06-03 09:59:40 +02:00
mika kuns
b748c1569e feat(ui): open Weekly Report modal from the menu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:56:32 +02:00
mika kuns
74fc39f1a6 feat(ui): WeeklyReportModalView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:55:10 +02:00
mika kuns
ccd2ee2cc7 feat(ui): WeeklyReportModalViewModel with default-range logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:54:10 +02:00
mika kuns
5b89e3d03f feat(settings): persist report excluded paths and standup weekday
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 09:50:03 +02:00
mika kuns
e106b00b16 feat(ui): WorkerClient methods for week report and daily notes 2026-06-03 09:46:39 +02:00
mika kuns
d7558ef451 feat(worker): hub methods for week report and daily notes 2026-06-03 09:44:45 +02:00
mika kuns
4aa4353d11 feat(worker): register report reader and service in DI 2026-06-03 09:43:48 +02:00
mika kuns
50d84f12c9 feat(worker): WeekReportService orchestrates generate + store 2026-06-03 09:42:21 +02:00
mika kuns
e2271b5a50 feat(worker): week report prompt builder (day-major pivot) 2026-06-03 09:40:57 +02:00
mika kuns
bec87b3d6f feat(worker): ClaudeHistoryReader distills session logs 2026-06-03 09:37:40 +02:00
mika kuns
4cb7ad8dfa feat(worker): report activity models and reader interface 2026-06-03 09:35:49 +02:00
mika kuns
992fbf0763 feat(data): add WeekReportRepository with tests 2026-06-03 09:34:03 +02:00
mika kuns
1d7b86dbef feat(data): add DailyNoteRepository with tests 2026-06-03 09:32:08 +02:00
mika kuns
036586e736 feat(data): migration for daily notes and week reports 2026-06-03 09:28:50 +02:00
mika kuns
d9e5d2600b feat(data): configure daily note + week report tables 2026-06-03 09:26:00 +02:00
mika kuns
10d86b4bd6 feat(data): add daily note + week report entities and report settings 2026-06-03 09:24:23 +02:00
mika kuns
f72cfae7d9 docs: add weekly report implementation plan 2026-06-03 09:19:08 +02:00
mika kuns
e5a2ed250d docs: add report prompt and day-major pivot to weekly report spec
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:52:25 +02:00
mika kuns
536d819328 docs: add weekly report feature design spec
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:40:19 +02:00
139 changed files with 8521 additions and 417 deletions

View File

@@ -6,6 +6,7 @@
<Project Path="src/ClaudeDo.Worker/ClaudeDo.Worker.csproj" />
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />
@@ -13,5 +14,6 @@
<Project Path="tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj" />
<Project Path="tests/ClaudeDo.Installer.Tests/ClaudeDo.Installer.Tests.csproj" />
<Project Path="tests/ClaudeDo.Releases.Tests/ClaudeDo.Releases.Tests.csproj" />
<Project Path="tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj" />
</Folder>
</Solution>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
# Localization (i18n) Support — Design
**Date:** 2026-06-03
**Status:** Approved (pending spec review)
## Goal
Add translation support to ClaudeDo. The user picks a language in the Settings modal and **all** UI text reflects it instantly (no restart). The WPF installer is localized the same way and gets its own language picker. Ship **English only** now, but the system is fully data-driven: adding a new language means dropping one JSON file into a folder — **no code changes, no rebuild**.
## Decisions (from brainstorming)
- **Languages:** English only at launch; extensible via translation files.
- **Switching:** Live / instant — all bound UI text updates the moment the language changes.
- **Storage:** Selected language stored in `~/.todo-app/ui.config.json` (the local UI config that also holds `DbPath`/`SignalRUrl`). Purely a UI concern — does **not** go through the worker/SignalR settings path.
- **Installer:** Defaults to existing config language (upgrade) → OS culture → English. Shows a language picker in the wizard, live-switches its own UI, and writes the chosen language into `ui.config.json` so the app launches matching the installer.
- **Locale files:** Loose `*.json` files in a `locales/` folder next to the running exe, scanned at startup to discover available languages.
- **Code sharing:** A shared `ClaudeDo.Localization` project holds the loading/lookup/language-list logic, referenced by `ClaudeDo.Ui`, `ClaudeDo.App`, and `ClaudeDo.Installer`. Each UI framework keeps its own thin markup-extension binding layer (Avalonia ≠ WPF).
## Architecture & Components
### New shared project: `ClaudeDo.Localization`
- **`LocaleStore`** — discovers and loads `*.json` files from the `locales/` folder next to the running exe. Parses each file's nested JSON, **flattens it into an internal `Dictionary<string,string>`** keyed by dot-path for O(1) lookup, and captures `metadata.code` / `metadata.name`. Exposes the list of available languages for the dropdowns.
- **`ILocalizer` / `Localizer`** — singleton holding the *active* language dictionary. Members:
- indexer `this[string key]` → translated string (with fallback),
- `string Get(string key, params object[] args)``string.Format` for parameterized strings,
- `void SetLanguage(string code)` → swaps the active dictionary and raises `PropertyChanged` for the indexer so **all live bindings refresh** (this is what enables instant switching),
- `AvailableLanguages` (list of `{ code, name }`), `CurrentCode`.
- **Fallback chain:** requested key in active language → same key in English → the key path string itself (a missing translation is visible, never a crash).
- **OS-culture resolution:** helper that maps the current OS UI culture to an available locale code, falling back to English.
### Per-framework binding layer (not shared)
- **Avalonia:** a `{loc:Tr Some.Key}` markup extension that binds to `Localizer[key]` (Source = the singleton `Localizer`, Path = `[key]`). Language change raises the indexer `PropertyChanged`, refreshing every binding.
- **WPF installer:** an equivalent markup extension doing the same against the installer's own `Localizer` instance.
Both consume the **same JSON files and the same `LocaleStore`/`Localizer` logic** from the shared project.
## Translation File Format
`locales/en.json` (and future `de.json`, `fr.json`, …) — nested, human-friendly hierarchy:
```json
{
"metadata": { "code": "en", "name": "English" },
"settings": {
"save": "Save",
"cancel": "Cancel",
"general": { "model": "Model", "maxParallel": "Max parallel executions" }
},
"tasks": {
"addPlaceholder": "Add a task…",
"overdue": "OVERDUE"
},
"worktrees": { "autoCleanupDays": "{0} days" }
}
```
- `metadata.code` is the language id stored in `ui.config.json` and matched to OS culture; `metadata.name` is the dropdown label.
- **Lookup by dot-path key** (`"settings.general.model"`). On-disk file stays grouped/nested; the runtime flattens it for fast lookup. Authors edit a clean hierarchy.
- **Parameters:** `{0}`, `{1}` placeholders resolved via `Get(key, args)`.
- **Encoding:** UTF-8 — non-ASCII languages work out of the box.
## Data Flow & Wiring
### App config
- Add `Language` (string, e.g. `"en"`) to `AppSettings` (`ClaudeDo.Ui/AppSettings.cs`) and to the installer mirror `InstallerAppSettings` (`ClaudeDo.Installer/Core/ConfigModels.cs`).
- Add a `Save()` method to `AppSettings` (today the UI only reads it).
### App startup (`ClaudeDo.App/Program.cs`)
1. `AppSettings.Load()` reads `Language` (missing/empty → resolve from OS culture, else `"en"`).
2. `LocaleStore` scans `locales/` next to the exe; `Localizer` is registered as a singleton and set to the configured language.
3. UI renders; every `{loc:Tr ...}` binding pulls from the active dictionary.
### Changing language in Settings (General tab)
- New "Language" dropdown bound to `Localizer.AvailableLanguages`; selection bound to current code.
- On change → `Localizer.SetLanguage(code)` (instant UI refresh) **and** `AppSettings.Language = code; AppSettings.Save()`. Local UI state only — not routed through worker/SignalR.
### Installer (`ClaudeDo.Installer`)
- On launch: default language = existing `ui.config.json` `Language` if present (upgrade), else OS culture, else English.
- Wizard gets a language dropdown (same `LocaleStore`, installer's own markup extension) → live-switches the installer UI.
- When writing `ui.config.json`, persists the chosen `Language` so the app launches matching the installer.
### Build wiring
- `locales/*.json` copied to output (`CopyToOutputDirectory`) for both App and Installer.
- Installer packages the `locales/` folder so it lands beside the installed exe.
## String-Extraction Scope
Mechanical but large; done screen-by-screen so each commit is reviewable, building one `en.json` as the single source of truth.
- **22 Avalonia `.axaml` views** — replace inline `Text="..."`, `Content="..."`, `PlaceholderText="..."`, and inline `ComboBoxItem` text with `{loc:Tr key}`.
- **ViewModel strings** — user-facing literals built in C# (e.g. `HeaderTitle`, `StatusPill`, status text, parameterized messages) resolve via injected `ILocalizer` (`localizer.Get(...)`). Log messages and non-user-facing strings stay as-is. **Live-switch note:** a VM string resolved once will not refresh on language change. For VM-built user-facing text, either (a) prefer resolving in XAML via `{loc:Tr}` where possible, or (b) have the VM subscribe to the `Localizer` change event and re-raise `PropertyChanged` (or re-resolve) for its localized properties. Decide per-property during extraction.
- **10 WPF installer files** — same treatment with the installer's markup extension; VM-driven headings (`Heading`, `NextButtonText`, etc.) go through `ILocalizer`.
- **Enum-ish display values** (model names, permission modes, weekday names) — translate the *display* text while keeping the underlying value/binding intact.
## Testing
- `ClaudeDo.Localization` unit tests: load/flatten nested JSON, dot-path lookup, fallback chain (active→en→key), `{0}` formatting, OS-culture resolution.
- `LocaleStore` discovery test (folder scan → available languages).
- **Key-coverage test:** every locale file's flattened key set matches `en.json`; fails the build if `en.json` drifts from other locale files.
- Settings round-trip test: `SetLanguage` updates `Localizer` **and** persists to `ui.config.json`.
- Manual UI pass (user's visual review): confirm instant switching with a throwaway `de.json` stub during dev, then remove it.
## Out of Scope (YAGNI)
- Pluralization rules, RTL layout, per-string gender.
- Translating the German weekly-report **body** (generated content — stays as-is).
- Localizing log output and non-user-facing strings.

View File

@@ -0,0 +1,226 @@
# Weekly Report — Design
**Date:** 2026-06-03
**Status:** Approved (pending spec review)
## Goal
Generate a short, standup-focused report of what the user did over the past week,
for the Wednesday standup. The report is built from the user's Claude Code session
history across all repos, distilled and summarized by Claude. Personal repos under a
configurable excluded path (default `C:\Private`) are left out. The user can author
per-day bullet notes inside ClaudeDo (via the My Day list) that are folded into the
report.
## Decisions (from brainstorming)
- **Data source:** all Claude Code history in `~/.claude/projects/*/*.jsonl`, both manual
sessions and ClaudeDo-run tasks, grouped by repo.
- **Exclusion:** a configurable list of path prefixes (default `["C:\\Private"]`). Any
session whose `cwd` starts with an excluded prefix is dropped.
- **Summarization:** Claude CLI summarizes. The Worker distills the logs, then runs a
single one-shot `claude -p` call via the existing `ClaudeProcess` and returns the
result markdown. No worktree, no task row, no queue.
- **Period:** default "since last Wednesday → today", computed from a configurable
standup weekday. The range is adjustable in the modal.
- **Signal fed to Claude:** user prompts (intent), assistant closing summaries, and the
user's daily notes. No git-commit scanning.
- **Report shape:** German, grouped by day, first-person past-tense bullets, ~3-5
bullets/day with trivia merged/dropped, notes blended into one deduplicated list per
day. See the Report Prompt section.
- **Placement:** a "Weekly Report" overlay modal opened from the toolbar, rendering via
the existing `MarkdownView`.
- **Output:** view-only in-app (no export).
- **Notes UI:** authored in the My Day list via a pinned non-task "Notes" pseudo-row that
repurposes the Details island into a bullet-notes editor. Per-day bullets with a day
navigator (prev/next arrows + date picker + Today).
- **Report persistence:** generated reports are stored, keyed by exact date range, and
reused. Generation is button-driven (never automatic); a Regenerate button overwrites.
## Architecture Overview
```
UI (WeeklyReportModal, Details-island notes mode)
│ SignalR
WorkerHub ── GetWeekReport / GenerateWeekReport / daily-notes CRUD
├── WeekReportService ──► ClaudeHistoryReader (scan ~/.claude/projects)
│ │ (distilled activity)
│ ├── DailyNoteRepository (notes in window)
│ ├── ClaudeProcess (one-shot summarize)
│ └── WeekReportRepository (store/reuse)
└── DailyNoteRepository (CRUD)
Data: DailyNoteEntity, WeekReportEntity + repositories + EF migration
AppSettingsEntity: ReportExcludedPaths, StandupWeekday
```
## Components
### 1. Data layer (`ClaudeDo.Data`)
**`DailyNoteEntity`** (table `daily_notes`)
- `Id` (GUID string, init-only PK)
- `Date` (date-only; the day the bullet belongs to)
- `Text` (string, the bullet content)
- `SortOrder` (int; ordering within a day)
- `CreatedAt` (DateTime)
**`DailyNoteRepository`** (async, CancellationToken, follows existing repo pattern)
- `ListByDayAsync(DateOnly day)` — bullets for one day, ordered by `SortOrder`.
- `ListBetweenAsync(DateOnly start, DateOnly end)` — bullets in a window (used by the report).
- `AddAsync(DateOnly day, string text)` — appends a bullet (assigns next `SortOrder`).
- `UpdateAsync(string id, string text)`
- `DeleteAsync(string id)`
**`WeekReportEntity`** (table `week_reports`)
- `Id` (GUID string, init-only PK)
- `StartDate`, `EndDate` (date-only; the report window — unique together)
- `Markdown` (string; the generated report)
- `GeneratedAt` (DateTime)
**`WeekReportRepository`**
- `GetByRangeAsync(DateOnly start, DateOnly end)` — stored report for an exact range, or null.
- `UpsertAsync(DateOnly start, DateOnly end, string markdown)` — insert or overwrite by range.
**`AppSettingsEntity`** — two new columns:
- `ReportExcludedPaths` (string, JSON array of path prefixes; default `["C:\\Private"]`)
- `StandupWeekday` (int, `DayOfWeek`; default `Wednesday` = 3)
**Migration** — one EF migration adds `daily_notes`, `week_reports`, and the two
`app_settings` columns. Entity configs in `Configuration/` (date-only and enum/JSON
conversion via `ValueConverter`, per existing convention).
### 2. Worker (`ClaudeDo.Worker`) — new `Report/` folder
**`ClaudeHistoryReader`** (raw → distilled)
- Input: date window + excluded path prefixes.
- Enumerates `~/.claude/projects/*/*.jsonl`.
- Parses each line as JSON; tolerant of malformed lines (skip, never throw).
- Drops a session entirely if its `cwd` starts with any excluded prefix
(case-insensitive, normalized separators).
- Keeps messages whose `timestamp` falls in `[start, end]`.
- Extracts, per repo (`cwd`) → per day:
- **user prompts**: `type == "user"` text content (string or `content[].text`).
Skip tool-result-only user turns and queue/attachment/hook noise.
- **assistant closing summaries**: the final assistant text block of each turn/session.
- Output: a structured model, e.g.
`IReadOnlyList<RepoActivity>` where `RepoActivity { RepoPath, Days: List<DayActivity{ Date, Prompts[], Summaries[] }> }`.
**`WeekReportService`** (distilled → stored summary)
- `GenerateAsync(start, end, ct)`:
1. Read settings (excluded paths, standup weekday).
2. `ClaudeHistoryReader` → distilled activity.
3. `DailyNoteRepository.ListBetweenAsync` → notes grouped by day.
4. Pivot the distilled activity (repo→day from the reader) into **day-major**
(day→repo) to match the day-grouped report, and build the prompt from the
template in the Report Prompt section. Empty window → produce a "no activity"
report without calling Claude.
5. Run `ClaudeProcess` once (`claude -p`, no worktree/agents; working dir = a neutral
dir). Read `RunResult.ResultMarkdown`.
6. `WeekReportRepository.UpsertAsync(start, end, markdown)`; return markdown.
7. On Claude failure, surface `RunResult.ErrorMarkdown` to the caller (do not store).
- `GetStoredAsync(start, end)``WeekReportRepository.GetByRangeAsync`.
Interfaces live in `Report/Interfaces/` per the area convention.
#### Report Prompt
`WeekReportService` assembles this prompt. Instructions are in English (more reliable
steering); the output is forced to German. `{...}` are filled at build time.
```
You are generating a concise weekly standup report for a software developer.
Summarize what they accomplished between {start:dd.MM.yyyy} and {end:dd.MM.yyyy}.
Rules:
- Write the ENTIRE report in German.
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
activity (German weekday names). Omit days with no activity entirely.
- Within each day: 35 first-person, past-tense bullets ("- Habe X umgesetzt",
"- Y behoben"). Merge related small work into one bullet.
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
- Blend the developer's own notes and the derived activity into ONE deduplicated
bullet list per day. The developer's notes are authoritative — never omit or
contradict their substance.
- Name the project/repo when it adds clarity.
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
== Activity (from session history) ==
{day-major: for each day → for each repo → its prompts + closing summaries}
== Developer notes ==
{day-major: for each day → the bullets}
```
### 3. IPC (Hub + WorkerClient)
**`WorkerHub`** new methods:
- `GetWeekReport(string startIso, string endIso)` → stored markdown or null.
- `GenerateWeekReport(string startIso, string endIso)` → generates, stores, returns markdown.
- `GetDailyNotes(string dayIso)` → bullets for a day.
- `AddDailyNote(string dayIso, string text)` → created bullet.
- `UpdateDailyNote(string id, string text)`.
- `DeleteDailyNote(string id)`.
**`WorkerClient`** (UI) mirrors these, following the existing
`WorkerPrimeScheduleApi`/AppSettings method pattern.
### 4. UI (`ClaudeDo.Ui`)
**Weekly Report modal** (`WeeklyReportModalView` + `WeeklyReportModalViewModel`)
- Overlay modal in the `Modals/` pattern (like `WorktreesOverviewModalView`),
registered in `IslandsShellViewModel`, opened from a new toolbar button.
- Date range: two `ThemedDatePicker`s, default "since last Wednesday → today" computed
from `StandupWeekday`.
- On open and on range change: call `GetWeekReport`.
- Stored report exists → render markdown via `MarkdownView`, show `GeneratedAt`, show
a **Regenerate** button.
- None → empty state ("Not generated yet") + a **Generate** button.
- **Generate**/**Regenerate**: call `GenerateWeekReport` with a busy/spinner state;
render the returned markdown. Generation only ever runs from these buttons.
- View-only; no export.
**Notes in My Day**
- The My Day smart list (`smart:my-day`) pins a fixed, non-task "Notes" pseudo-row at
the top, recognized by the list/selection code (not a `TaskEntity`).
- Selecting it puts the **Details island** into **notes mode** (task fields hidden,
notes editor shown). The island hosts a dedicated `NotesEditorViewModel` + small view
rather than swelling `DetailsIslandViewModel` (already ~978 lines); the bullet logic
stays isolated and testable.
- **Day navigator** in the editor header: `<` / `>` arrows to step days, a
`ThemedDatePicker` to jump to any date, and a "Today" button. Defaults to today; the
pinned row's default day rolls over at midnight (no data lost — past days remain
reachable via the navigator).
- **Bullet editing** for the selected day: list of bullets with add / inline-edit /
delete / reorder (`SortOrder`). Each operation goes through the daily-notes hub CRUD.
### 5. Settings
- Add the excluded-path list and the standup weekday to the existing Settings modal,
persisted via the new `app_settings` columns and the existing
`GetAppSettings`/`UpdateAppSettings` path.
## Error Handling
- Malformed/unreadable JSONL lines are skipped, never fatal.
- Empty window → a "no activity" report, no Claude call.
- Claude call failure → error surfaced in the modal; nothing stored.
- Date ranges normalized to date-only; the stored report key is the exact (start, end).
## Testing
- **`ClaudeHistoryReader`** (Worker tests, fixture `.jsonl`): date-window filtering,
excluded-prefix dropping (case/separator normalization), prompt/summary extraction,
malformed-line tolerance, repo/day grouping.
- **`WeekReportService`**: prompt-building from distilled activity + notes; empty-window
short-circuit; storage upsert; with a faked `ClaudeProcess`.
- **`DailyNoteRepository`** and **`WeekReportRepository`**: CRUD / upsert / range lookup
against real SQLite (matches existing test style).
## Out of Scope
- Report export (clipboard/file) — view-only for now.
- Git-commit scanning.
- Editing or summarizing full transcripts; only prompts + closing summaries are used.

View File

@@ -28,5 +28,7 @@
<ItemGroup>
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
</ItemGroup>
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
</Project>

View File

@@ -1,9 +1,12 @@
using Avalonia;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Localization;
using ClaudeDo.Releases;
using ClaudeDo.Ui;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -11,6 +14,9 @@ using ClaudeDo.Ui.ViewModels.Modals.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -70,6 +76,18 @@ sealed class Program
// Infrastructure
sc.AddSingleton(settings);
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
var localeStore = LocaleStore.Load(localesDir);
var initialLang = !string.IsNullOrWhiteSpace(settings.Language)
? settings.Language
: CultureResolver.Resolve(
CultureInfo.CurrentUICulture.Name,
localeStore.Available.Select(l => l.Code).ToArray(),
fallback: "en");
var localizer = new Localizer(localeStore, initialLang);
TrExtension.Localizer = localizer;
ClaudeDo.Ui.Localization.Loc.Current = localizer;
sc.AddSingleton<ILocalizer>(localizer);
sc.AddDbContextFactory<ClaudeDoDbContext>(opt =>
opt.UseSqlite($"Data Source={dbPath}"));
sc.AddScoped<ClaudeDoDbContext>(sp =>
@@ -100,6 +118,7 @@ sealed class Program
sc.AddTransient<WorktreesOverviewModalViewModel>();
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
sc.AddSingleton<INotesApi, WorkerNotesApi>();
sc.AddTransient<PrimeClaudeTabViewModel>();
sc.AddTransient<SettingsModalViewModel>();
sc.AddTransient<MergeModalViewModel>();
@@ -107,6 +126,8 @@ sealed class Program
sc.AddTransient<ListSettingsModalViewModel>();
sc.AddTransient<RepoImportModalViewModel>();
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
sc.AddTransient<WeeklyReportModalViewModel>();
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
// Islands shell VMs
sc.AddSingleton<ListsIslandViewModel>(sp =>
@@ -122,7 +143,8 @@ sealed class Program
new DetailsIslandViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<WorkerClient>(),
sp));
sp,
sp.GetRequiredService<INotesApi>()));
sc.AddSingleton<IslandsShellViewModel>();
return sc.BuildServiceProvider();

View File

@@ -10,6 +10,9 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`) and `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`)
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
## Repositories
@@ -20,6 +23,8 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
## Infrastructure
@@ -34,7 +39,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
## Schema
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`. 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`).
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns.
## Conventions

View File

@@ -19,6 +19,8 @@ public class ClaudeDoDbContext : DbContext
public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
public DbSet<AppSettingsEntity> AppSettings => Set<AppSettingsEntity>();
public DbSet<PrimeScheduleEntity> PrimeSchedules => Set<PrimeScheduleEntity>();
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

View File

@@ -37,6 +37,10 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
builder.Property(s => s.RepoImportFolders)
.HasColumnName("repo_import_folders");
builder.Property(s => s.ReportExcludedPaths).HasColumnName("report_excluded_paths");
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
}
}

View File

@@ -0,0 +1,20 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class DailyNoteEntityConfiguration : IEntityTypeConfiguration<DailyNoteEntity>
{
public void Configure(EntityTypeBuilder<DailyNoteEntity> builder)
{
builder.ToTable("daily_notes");
builder.HasKey(n => n.Id);
builder.Property(n => n.Id).HasColumnName("id").ValueGeneratedNever();
builder.Property(n => n.Date).HasColumnName("note_date").IsRequired();
builder.Property(n => n.Text).HasColumnName("text").IsRequired();
builder.Property(n => n.SortOrder).HasColumnName("sort_order").IsRequired();
builder.Property(n => n.CreatedAt).HasColumnName("created_at").IsRequired();
builder.HasIndex(n => n.Date);
}
}

View File

@@ -0,0 +1,20 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class WeekReportEntityConfiguration : IEntityTypeConfiguration<WeekReportEntity>
{
public void Configure(EntityTypeBuilder<WeekReportEntity> builder)
{
builder.ToTable("week_reports");
builder.HasKey(r => r.Id);
builder.Property(r => r.Id).HasColumnName("id").ValueGeneratedNever();
builder.Property(r => r.StartDate).HasColumnName("start_date").IsRequired();
builder.Property(r => r.EndDate).HasColumnName("end_date").IsRequired();
builder.Property(r => r.Markdown).HasColumnName("markdown").IsRequired();
builder.Property(r => r.GeneratedAt).HasColumnName("generated_at").IsRequired();
builder.HasIndex(r => new { r.StartDate, r.EndDate }).IsUnique();
}
}

View File

@@ -0,0 +1,675 @@
// <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("20260603072822_WeeklyReport")]
partial class WeeklyReport
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
b.Property<string>("ReportExcludedPaths")
.HasColumnType("TEXT")
.HasColumnName("report_excluded_paths");
b.Property<int>("StandupWeekday")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
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<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("Days")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(31)
.HasColumnName("days_of_week");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("enabled");
b.Property<DateTimeOffset?>("LastRunAt")
.HasColumnType("TEXT")
.HasColumnName("last_run_at");
b.Property<string>("PromptOverride")
.HasColumnType("TEXT")
.HasColumnName("prompt_override");
b.Property<TimeSpan>("TimeOfDay")
.HasColumnType("TEXT")
.HasColumnName("time_of_day");
b.HasKey("Id");
b.ToTable("prime_schedules", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<string>("ReviewFeedback")
.HasColumnType("TEXT")
.HasColumnName("review_feedback");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateOnly>("EndDate")
.HasColumnType("TEXT")
.HasColumnName("end_date");
b.Property<DateTime>("GeneratedAt")
.HasColumnType("TEXT")
.HasColumnName("generated_at");
b.Property<string>("Markdown")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("markdown");
b.Property<DateOnly>("StartDate")
.HasColumnType("TEXT")
.HasColumnName("start_date");
b.HasKey("Id");
b.HasIndex("StartDate", "EndDate")
.IsUnique();
b.ToTable("week_reports", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class WeeklyReport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "report_excluded_paths",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "standup_weekday",
table: "app_settings",
type: "INTEGER",
nullable: false,
defaultValue: 3);
migrationBuilder.CreateTable(
name: "daily_notes",
columns: table => new
{
id = table.Column<string>(type: "TEXT", nullable: false),
note_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
text = table.Column<string>(type: "TEXT", nullable: false),
sort_order = table.Column<int>(type: "INTEGER", nullable: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_daily_notes", x => x.id);
});
migrationBuilder.CreateTable(
name: "week_reports",
columns: table => new
{
id = table.Column<string>(type: "TEXT", nullable: false),
start_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
end_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
markdown = table.Column<string>(type: "TEXT", nullable: false),
generated_at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_week_reports", x => x.id);
});
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
columns: new[] { "report_excluded_paths", "standup_weekday" },
values: new object[] { null, 3 });
migrationBuilder.CreateIndex(
name: "IX_daily_notes_note_date",
table: "daily_notes",
column: "note_date");
migrationBuilder.CreateIndex(
name: "IX_week_reports_start_date_end_date",
table: "week_reports",
columns: new[] { "start_date", "end_date" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "daily_notes");
migrationBuilder.DropTable(
name: "week_reports");
migrationBuilder.DropColumn(
name: "report_excluded_paths",
table: "app_settings");
migrationBuilder.DropColumn(
name: "standup_weekday",
table: "app_settings");
}
}
}

View File

@@ -64,6 +64,16 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
b.Property<string>("ReportExcludedPaths")
.HasColumnType("TEXT")
.HasColumnName("report_excluded_paths");
b.Property<int>("StandupWeekday")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -96,12 +106,43 @@ namespace ClaudeDo.Data.Migrations
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")
@@ -465,6 +506,37 @@ namespace ClaudeDo.Data.Migrations
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")

View File

@@ -20,4 +20,8 @@ public sealed class AppSettingsEntity
// JSON array of parent folders remembered by the repo-import modal.
public string? RepoImportFolders { get; set; }
// JSON array of path prefixes whose sessions are excluded from the weekly report.
public string? ReportExcludedPaths { get; set; }
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
public int StandupWeekday { get; set; } = (int)DayOfWeek.Wednesday;
}

View File

@@ -0,0 +1,10 @@
namespace ClaudeDo.Data.Models;
public sealed class DailyNoteEntity
{
public string Id { get; init; } = Guid.NewGuid().ToString();
public DateOnly Date { get; set; }
public string Text { get; set; } = string.Empty;
public int SortOrder { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,10 @@
namespace ClaudeDo.Data.Models;
public sealed class WeekReportEntity
{
public string Id { get; init; } = Guid.NewGuid().ToString();
public DateOnly StartDate { get; set; }
public DateOnly EndDate { get; set; }
public string Markdown { get; set; } = string.Empty;
public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -50,6 +50,9 @@ public sealed class AppSettingsRepository
? null : updated.CentralWorktreeRoot;
row.WorktreeAutoCleanupEnabled = updated.WorktreeAutoCleanupEnabled;
row.WorktreeAutoCleanupDays = updated.WorktreeAutoCleanupDays;
row.ReportExcludedPaths = string.IsNullOrWhiteSpace(updated.ReportExcludedPaths)
? null : updated.ReportExcludedPaths;
row.StandupWeekday = updated.StandupWeekday;
await _context.SaveChangesAsync(ct);
}

View File

@@ -0,0 +1,59 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Repositories;
public sealed class DailyNoteRepository
{
private readonly ClaudeDoDbContext _context;
public DailyNoteRepository(ClaudeDoDbContext context) => _context = context;
public async Task<IReadOnlyList<DailyNoteEntity>> ListByDayAsync(DateOnly day, CancellationToken ct = default) =>
await _context.DailyNotes.AsNoTracking()
.Where(n => n.Date == day)
.OrderBy(n => n.SortOrder)
.ToListAsync(ct);
public async Task<IReadOnlyList<DailyNoteEntity>> ListBetweenAsync(
DateOnly start, DateOnly end, CancellationToken ct = default) =>
await _context.DailyNotes.AsNoTracking()
.Where(n => n.Date >= start && n.Date <= end)
.OrderBy(n => n.Date).ThenBy(n => n.SortOrder)
.ToListAsync(ct);
public async Task<DailyNoteEntity> AddAsync(DateOnly day, string text, CancellationToken ct = default)
{
var nextOrder = await _context.DailyNotes
.Where(n => n.Date == day)
.Select(n => (int?)n.SortOrder)
.MaxAsync(ct) ?? -1;
var note = new DailyNoteEntity
{
Date = day,
Text = text,
SortOrder = nextOrder + 1,
CreatedAt = DateTime.UtcNow,
};
_context.DailyNotes.Add(note);
await _context.SaveChangesAsync(ct);
return note;
}
public async Task UpdateAsync(string id, string text, CancellationToken ct = default)
{
var row = await _context.DailyNotes.FirstOrDefaultAsync(n => n.Id == id, ct);
if (row is null) return;
row.Text = text;
await _context.SaveChangesAsync(ct);
}
public async Task DeleteAsync(string id, CancellationToken ct = default)
{
var row = await _context.DailyNotes.FirstOrDefaultAsync(n => n.Id == id, ct);
if (row is null) return;
_context.DailyNotes.Remove(row);
await _context.SaveChangesAsync(ct);
}
}

View File

@@ -0,0 +1,38 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Repositories;
public sealed class WeekReportRepository
{
private readonly ClaudeDoDbContext _context;
public WeekReportRepository(ClaudeDoDbContext context) => _context = context;
public async Task<WeekReportEntity?> GetByRangeAsync(
DateOnly start, DateOnly end, CancellationToken ct = default) =>
await _context.WeekReports.AsNoTracking()
.FirstOrDefaultAsync(r => r.StartDate == start && r.EndDate == end, ct);
public async Task UpsertAsync(DateOnly start, DateOnly end, string markdown, CancellationToken ct = default)
{
var row = await _context.WeekReports
.FirstOrDefaultAsync(r => r.StartDate == start && r.EndDate == end, ct);
if (row is null)
{
_context.WeekReports.Add(new WeekReportEntity
{
StartDate = start,
EndDate = end,
Markdown = markdown,
GeneratedAt = DateTime.UtcNow,
});
}
else
{
row.Markdown = markdown;
row.GeneratedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync(ct);
}
}

View File

@@ -1,7 +1,12 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using ClaudeDo.Localization;
using ClaudeDo.Releases;
using ClaudeDo.Installer.Pages.InstallPage;
using ClaudeDo.Installer.Pages.PathsPage;
@@ -22,6 +27,17 @@ public partial class App : Application
{
base.OnStartup(e);
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
var localeStore = LocaleStore.Load(localesDir);
var existingSettings = InstallerAppSettings.Load();
var initialLang = !string.IsNullOrWhiteSpace(existingSettings.Language)
? existingSettings.Language
: CultureResolver.Resolve(CultureInfo.CurrentUICulture.Name,
localeStore.Available.Select(l => l.Code).ToArray(), "en");
var localizer = new Localizer(localeStore, initialLang);
TrExtension.Localizer = localizer;
// --- Self-update pre-flight ---
// Resolve current exe path. Assembly.Location may point to a .dll for apphost-based
// .NET apps; swap to the .exe companion when that happens.
@@ -120,7 +136,7 @@ public partial class App : Application
// --- Existing wizard start-up unchanged below this line ---
_services = BuildServices();
_services = BuildServices(localizer);
var context = _services.GetRequiredService<InstallContext>();
context.InstallerVersion = GetInstallerVersion();
@@ -183,9 +199,10 @@ public partial class App : Application
return infoAttr?.InformationalVersion ?? "0.0.0";
}
private static ServiceProvider BuildServices()
private static ServiceProvider BuildServices(ILocalizer localizer)
{
var sc = new ServiceCollection();
sc.AddSingleton(localizer);
// Core
sc.AddSingleton<InstallContext>();

View File

@@ -46,6 +46,9 @@
<ItemGroup>
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
<ProjectReference Include="..\ClaudeDo.Releases\ClaudeDo.Releases.csproj" />
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
</ItemGroup>
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
</Project>

View File

@@ -77,6 +77,7 @@ public sealed class InstallerAppSettings
{
public string DbPath { get; set; } = "~/.todo-app/todo.db";
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
public string Language { get; set; } = "";
private static readonly JsonSerializerOptions ReadOpts = new()
{

View File

@@ -36,4 +36,7 @@ public sealed class InstallContext
// WelcomePage — register the external MCP endpoint with the Claude CLI.
public bool RegisterMcpWithClaude { get; set; } = true;
public int ExternalMcpPort { get; set; } = 47_822;
// Language selection (persisted to ui.config.json)
public string Language { get; set; } = "";
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel;
using ClaudeDo.Localization;
namespace ClaudeDo.Installer.Localization;
public sealed class LocalizedString : INotifyPropertyChanged
{
private readonly ILocalizer _localizer;
private readonly string _key;
public LocalizedString(ILocalizer localizer, string key)
{
_localizer = localizer;
_key = key;
_localizer.LanguageChanged += (_, _) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
public string Value => _localizer[_key];
public event PropertyChangedEventHandler? PropertyChanged;
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Windows.Data;
using System.Windows.Markup;
using ClaudeDo.Localization;
namespace ClaudeDo.Installer.Localization;
public sealed class TrExtension : MarkupExtension
{
public TrExtension() { }
public TrExtension(string key) => Key = key;
public string Key { get; set; } = "";
public static ILocalizer? Localizer { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var loc = Localizer ?? throw new InvalidOperationException("TrExtension.Localizer not initialized");
var binding = new Binding(nameof(LocalizedString.Value))
{
Source = new LocalizedString(loc, Key),
Mode = BindingMode.OneWay
};
return binding.ProvideValue(serviceProvider);
}
}

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.InstallPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:InstallPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -17,8 +18,8 @@
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,16">
<TextBlock Text="Installation" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Click Install to build and deploy ClaudeDo."
<TextBlock Text="{loc:Tr installer.install.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.install.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap"/>
</StackPanel>
@@ -89,11 +90,11 @@
<!-- Action buttons -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="Cancel" Command="{Binding CancelInstallCommand}"
<Button Content="{loc:Tr installer.nav.cancel}" Command="{Binding CancelInstallCommand}"
Visibility="{Binding IsInstalling, Converter={StaticResource BoolToVisConverter}}"
Margin="0,0,8,0"/>
<Button Content="Launch ClaudeDo" Command="{Binding LaunchAppCommand}"
<Button Content="{loc:Tr installer.install.launch}" Command="{Binding LaunchAppCommand}"
Style="{StaticResource AccentButton}"
Visibility="{Binding IsComplete, Converter={StaticResource BoolToVisConverter}}"/>
</StackPanel>

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -29,7 +30,7 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
private InstallPageView? _view;
private CancellationTokenSource? _cts;
public string Title => "Install";
public string Title => TrExtension.Localizer?["installer.install.title"] ?? "Install";
public string Icon => "\uE896";
public int Order => 99;
public bool ShowInWizard => true;

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.PathsPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:PathsPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,28 +10,28 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="Data Paths" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure where ClaudeDo stores its data."
<TextBlock Text="{loc:Tr installer.paths.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.paths.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<Label Content="Database Path"/>
<Label Content="{loc:Tr installer.paths.databasePath}"/>
<TextBox Text="{Binding DbPath, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Log Directory"/>
<Label Content="{loc:Tr installer.paths.logDirectory}"/>
<TextBox Text="{Binding LogRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Sandbox Root"/>
<Label Content="{loc:Tr installer.paths.sandboxRoot}"/>
<TextBox Text="{Binding SandboxRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Worktree Strategy"/>
<Label Content="{loc:Tr installer.paths.worktreeStrategy}"/>
<ComboBox SelectedItem="{Binding WorktreeRootStrategy}" Margin="0,0,0,12">
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">sibling</sys:String>
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">central</sys:String>
</ComboBox>
<StackPanel Visibility="{Binding IsCentralVisible, Converter={StaticResource BoolToVisConverter}}">
<Label Content="Central Worktree Root"/>
<Label Content="{loc:Tr installer.paths.centralWorktreeRoot}"/>
<TextBox Text="{Binding CentralWorktreeRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
</StackPanel>

View File

@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Installer.Pages.PathsPage;
@@ -9,7 +10,7 @@ public partial class PathsPageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private PathsPageView? _view;
public string Title => "Paths";
public string Title => TrExtension.Localizer?["installer.paths.title"] ?? "Paths";
public string Icon => "\uE8B7";
public int Order => 1;
public bool ShowInWizard => true;

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.ServicePage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:ServicePageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,37 +10,37 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="Worker" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure the ClaudeDo background worker."
<TextBlock Text="{loc:Tr installer.service.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.service.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<Label Content="SignalR Port"/>
<Label Content="{loc:Tr installer.service.signalRPort}"/>
<TextBox Text="{Binding SignalRPort, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Queue Backstop Interval (ms)"/>
<Label Content="{loc:Tr installer.service.queueBackstopInterval}"/>
<TextBox Text="{Binding QueueBackstopIntervalMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Claude CLI Path"/>
<Label Content="{loc:Tr installer.service.claudeCliPath}"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding ClaudeBin, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseClaudeCommand}"
<Button Grid.Column="1" Content="{loc:Tr installer.nav.browse}" Command="{Binding BrowseClaudeCommand}"
Margin="8,0,0,0"/>
</Grid>
<Separator Margin="0,4,0,12"/>
<TextBlock Text="The worker runs as you (the logged-in user) via a per-user logon task, so it can use your Claude CLI authentication."
<TextBlock Text="{loc:Tr installer.service.autostartHint}"
Foreground="{StaticResource TextDimBrush}" FontSize="11" Margin="0,0,0,12"
TextWrapping="Wrap"/>
<CheckBox Content="Start worker automatically at logon" IsChecked="{Binding AutoStart}" Margin="0,0,0,12"/>
<CheckBox Content="{loc:Tr installer.service.autostart}" IsChecked="{Binding AutoStart}" Margin="0,0,0,12"/>
<Label Content="Restart Delay (ms)"/>
<Label Content="{loc:Tr installer.service.restartDelay}"/>
<TextBox Text="{Binding RestartDelayMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<TextBlock Text="{Binding ValidationError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"

View File

@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
@@ -11,7 +12,7 @@ public partial class ServicePageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private ServicePageView? _view;
public string Title => "Service";
public string Title => TrExtension.Localizer?["installer.service.title"] ?? "Service";
public string Icon => "\uE912";
public int Order => 2;
public bool ShowInWizard => true;

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.UiSettingsPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:UiSettingsPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,22 +10,22 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="UI Settings" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure the ClaudeDo desktop UI connection settings."
<TextBlock Text="{loc:Tr installer.uiSettings.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.uiSettings.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<CheckBox Content="Sync with service settings" IsChecked="{Binding IsSynced}" Margin="0,0,0,16"/>
<CheckBox Content="{loc:Tr installer.uiSettings.syncWithService}" IsChecked="{Binding IsSynced}" Margin="0,0,0,16"/>
<Label Content="SignalR URL"/>
<Label Content="{loc:Tr installer.uiSettings.signalRUrl}"/>
<TextBox Text="{Binding SignalRUrl, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
<Label Content="Database Path"/>
<Label Content="{loc:Tr installer.paths.databasePath}"/>
<TextBox Text="{Binding UiDbPath, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
<TextBlock Text="When synced, these values are derived from the Service and Paths pages."
<TextBlock Text="{loc:Tr installer.uiSettings.syncHint}"
Foreground="{StaticResource TextDimBrush}" FontSize="11" TextWrapping="Wrap"
Visibility="{Binding IsSynced, Converter={StaticResource BoolToVisConverter}}"
Margin="0,0,0,12"/>

View File

@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Installer.Pages.UiSettingsPage;
@@ -9,7 +10,7 @@ public partial class UiSettingsPageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private UiSettingsPageView? _view;
public string Title => "UI Settings";
public string Title => TrExtension.Localizer?["installer.uiSettings.title"] ?? "UI Settings";
public string Icon => "\uE771";
public int Order => 3;
public bool ShowInWizard => true;

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.WelcomePage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:WelcomePageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -14,7 +15,7 @@
<TextBlock Text="{Binding Subheading}" TextWrapping="Wrap"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,24"/>
<Label Content="Install Directory"/>
<Label Content="{loc:Tr installer.welcome.installDirectory}"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -24,7 +25,7 @@
Text="{Binding InstallDirectory, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding InstallDirEditable}"/>
<Button Grid.Column="1"
Content="Browse..."
Content="{loc:Tr installer.nav.browse}"
Margin="8,0,0,0"
Command="{Binding BrowseInstallCommand}"
IsEnabled="{Binding InstallDirEditable}"/>
@@ -32,10 +33,10 @@
<TextBlock Text="{Binding InstallError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
Visibility="{Binding InstallError, Converter={StaticResource NullToCollapsedConverter}}"/>
<CheckBox Content="Register MCP server with Claude"
<CheckBox Content="{loc:Tr installer.welcome.registerMcp}"
IsChecked="{Binding RegisterMcp}"
Margin="0,24,0,0"/>
<TextBlock Text="Runs 'claude mcp add' so Claude can view and manage your ClaudeDo tasks. You can change this later."
<TextBlock Text="{loc:Tr installer.welcome.registerMcpHint}"
TextWrapping="Wrap" FontSize="11"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,4,0,0"/>

View File

@@ -1,6 +1,7 @@
using System.IO;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
@@ -12,7 +13,7 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private WelcomePageView? _view;
public string Title => "Welcome";
public string Title => TrExtension.Localizer?["installer.welcome.title"] ?? "Welcome";
public string Icon => "\uE80F";
public int Order => 0;
public bool ShowInWizard => true;
@@ -37,17 +38,18 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
? @"C:\Program Files\ClaudeDo"
: _context.InstallDirectory;
var loc = TrExtension.Localizer;
switch (_context.Mode)
{
case InstallerMode.FreshInstall:
Heading = "Install ClaudeDo";
Subheading = "Choose where to install ClaudeDo, then click Next.";
Heading = loc?["installer.welcome.heading"] ?? "Install ClaudeDo";
Subheading = loc?["installer.welcome.subheading"] ?? "Choose where to install ClaudeDo, then click Next.";
InstallDirEditable = true;
break;
case InstallerMode.Update:
Heading = $"Update ClaudeDo {_context.InstalledVersion ?? "?"} -> {_context.LatestVersion ?? "?"}";
Subheading = "Your tasks, config, and database will be preserved. Click Next to continue.";
Subheading = loc?["installer.welcome.updateSubheading"] ?? "Your tasks, config, and database will be preserved. Click Next to continue.";
InstallDirEditable = false; // stay where we were installed
break;

View File

@@ -31,6 +31,7 @@ public sealed class WriteConfigStep : IInstallStep
{
DbPath = Paths.Expand(ctx.UiDbPath),
SignalRUrl = ctx.SignalRUrl,
Language = ctx.Language,
};
uiCfg.Save();
progress.Report("Written ui.config.json");

View File

@@ -1,6 +1,7 @@
<Window x:Class="ClaudeDo.Installer.Views.SelfUpdatePromptWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Installer Update"
Width="460" Height="200"
WindowStartupLocation="CenterScreen"
@@ -13,13 +14,13 @@
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="16" FontWeight="SemiBold" Text="A newer installer is available"/>
<TextBlock Grid.Row="0" FontSize="16" FontWeight="SemiBold" Text="{loc:Tr installer.selfUpdate.heading}"/>
<TextBlock Grid.Row="1" Margin="0,8,0,0" TextWrapping="Wrap" x:Name="DetailText"/>
<TextBlock Grid.Row="2" Margin="0,12,0,0" TextWrapping="Wrap" Foreground="#a0a0a0" x:Name="ProgressText" Visibility="Collapsed"/>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="UpdateBtn" Content="Update" MinWidth="90" Margin="4,0" Padding="10,4" Click="UpdateBtn_Click" IsDefault="True"/>
<Button x:Name="ContinueBtn" Content="Continue anyway" MinWidth="140" Margin="4,0" Padding="10,4" Click="ContinueBtn_Click"/>
<Button x:Name="CancelBtn" Content="Cancel" MinWidth="90" Margin="4,0" Padding="10,4" Click="CancelBtn_Click" IsCancel="True"/>
<Button x:Name="UpdateBtn" Content="{loc:Tr installer.selfUpdate.update}" MinWidth="90" Margin="4,0" Padding="10,4" Click="UpdateBtn_Click" IsDefault="True"/>
<Button x:Name="ContinueBtn" Content="{loc:Tr installer.selfUpdate.continueAnyway}" MinWidth="140" Margin="4,0" Padding="10,4" Click="ContinueBtn_Click"/>
<Button x:Name="CancelBtn" Content="{loc:Tr installer.nav.cancel}" MinWidth="90" Margin="4,0" Padding="10,4" Click="CancelBtn_Click" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -1,5 +1,6 @@
using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Localization;
using ClaudeDo.Releases;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -10,6 +11,7 @@ namespace ClaudeDo.Installer.Views;
public partial class SettingsViewModel : ObservableObject
{
private readonly InstallContext _context;
private readonly ILocalizer _localizer;
private readonly IReleaseClient _releases;
private readonly StopWorkerStep _stopService;
private readonly StartWorkerStep _startService;
@@ -37,6 +39,7 @@ public partial class SettingsViewModel : ObservableObject
public SettingsViewModel(
PageResolver resolver,
InstallContext context,
ILocalizer localizer,
IReleaseClient releases,
StopWorkerStep stopService,
StartWorkerStep startService,
@@ -46,6 +49,7 @@ public partial class SettingsViewModel : ObservableObject
{
Pages = resolver.SettingsPages;
_context = context;
_localizer = localizer;
_releases = releases;
_stopService = stopService;
_startService = startService;
@@ -104,6 +108,7 @@ public partial class SettingsViewModel : ObservableObject
{
DbPath = _context.UiDbPath,
SignalRUrl = _context.SignalRUrl,
Language = _localizer.CurrentCode,
};
uiCfg.Save();

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Settings"
Icon="/ClaudeTaskSetup.ico"
Width="720" Height="520"
@@ -90,16 +91,16 @@
</StackPanel>
<CheckBox Grid.Column="1" IsChecked="{Binding RemoveAppData}"
Content="Remove user data (tasks, logs, configs in ~/.todo-app)"
Content="{loc:Tr installer.settings.removeUserData}"
Margin="0,0,12,0" VerticalAlignment="Center"/>
<Button Grid.Column="2" Content="Uninstall" Margin="0,0,8,0"
<Button Grid.Column="2" Content="{loc:Tr installer.settings.uninstall}" Margin="0,0,8,0"
Command="{Binding UninstallCommand}"/>
<Button Grid.Column="3" Content="Repair" Margin="0,0,8,0"
<Button Grid.Column="3" Content="{loc:Tr installer.settings.repair}" Margin="0,0,8,0"
Command="{Binding RepairCommand}"/>
<Button Grid.Column="4" Content="Save" Margin="0,0,8,0"
<Button Grid.Column="4" Content="{loc:Tr installer.settings.save}" Margin="0,0,8,0"
Command="{Binding SaveCommand}"
Style="{StaticResource AccentButton}"/>
<Button Grid.Column="5" Content="Close"
<Button Grid.Column="5" Content="{loc:Tr installer.settings.close}"
Command="{Binding CloseCommand}"/>
</Grid>
</Border>

View File

@@ -3,6 +3,7 @@ using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Pages.InstallPage;
using ClaudeDo.Installer.Pages.WelcomePage;
using ClaudeDo.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -11,8 +12,20 @@ namespace ClaudeDo.Installer.Views;
public partial class WizardViewModel : ObservableObject
{
private readonly InstallContext _context;
private readonly ILocalizer _localizer;
public IReadOnlyList<IInstallerPage> Pages { get; }
public IReadOnlyList<LanguageOption> Languages { get; }
[ObservableProperty]
private LanguageOption? _selectedLanguage;
partial void OnSelectedLanguageChanged(LanguageOption? value)
{
if (value is null) return;
_localizer.SetLanguage(value.Value.Code);
_context.Language = value.Value.Code;
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanGoBack))]
@@ -24,14 +37,20 @@ public partial class WizardViewModel : ObservableObject
public IInstallerPage CurrentPage => Pages[CurrentPageIndex];
public bool CanGoBack => CurrentPageIndex > 0;
public bool IsLastPage => CurrentPageIndex == Pages.Count - 1;
public string NextButtonText => IsLastPage ? "Install" : "Next \u2192";
public string NextButtonText => IsLastPage
? (_localizer["installer.nav.install"])
: (_localizer["installer.nav.next"]);
[ObservableProperty]
private string? _validationError;
public WizardViewModel(PageResolver resolver, InstallContext context)
public WizardViewModel(PageResolver resolver, InstallContext context, ILocalizer localizer)
{
_context = context;
_localizer = localizer;
Languages = localizer.AvailableLanguages;
_selectedLanguage = Languages.FirstOrDefault(l => l.Code == localizer.CurrentCode);
_context.Language = localizer.CurrentCode;
var all = resolver.WizardPages;
Pages = context.Mode == InstallerMode.Update

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Installer"
Icon="/ClaudeTaskSetup.ico"
Width="720" Height="520"
@@ -27,6 +28,12 @@
<Border Grid.Row="0" Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1"
Padding="20,14">
<DockPanel>
<ComboBox DockPanel.Dock="Right"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}"
DisplayMemberPath="Name"
Width="150" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<ItemsControl ItemsSource="{Binding Pages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -61,6 +68,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Border>
<!-- Page Content -->
@@ -85,7 +93,7 @@
VerticalAlignment="Center" FontSize="12"
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
<Button Grid.Column="1" Content="Back"
<Button Grid.Column="1" Content="{loc:Tr installer.nav.back}"
Command="{Binding GoBackCommand}"
IsEnabled="{Binding CanGoBack}"
Margin="0,0,8,0" MinWidth="80"/>

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="ClaudeDo.Localization.Tests" />
</ItemGroup>
<Import Project="Locales.targets" />
</Project>

View File

@@ -0,0 +1,16 @@
namespace ClaudeDo.Localization;
public static class CultureResolver
{
public static string Resolve(string cultureName, IReadOnlyCollection<string> available, string fallback = "en")
{
if (string.IsNullOrWhiteSpace(cultureName)) return fallback;
var exact = available.FirstOrDefault(c => string.Equals(c, cultureName, StringComparison.OrdinalIgnoreCase));
if (exact is not null) return exact;
var primary = cultureName.Split('-')[0];
var byPrimary = available.FirstOrDefault(c => string.Equals(c, primary, StringComparison.OrdinalIgnoreCase));
return byPrimary ?? fallback;
}
}

View File

@@ -0,0 +1,13 @@
namespace ClaudeDo.Localization;
public readonly record struct LanguageOption(string Code, string Name);
public interface ILocalizer
{
string this[string key] { get; }
string Get(string key, params object[] args);
string CurrentCode { get; }
IReadOnlyList<LanguageOption> AvailableLanguages { get; }
void SetLanguage(string code);
event EventHandler? LanguageChanged;
}

View File

@@ -0,0 +1,15 @@
namespace ClaudeDo.Localization;
public sealed class LocaleFile
{
public LocaleFile(string code, string name, IReadOnlyDictionary<string, string> strings)
{
Code = code;
Name = name;
Strings = strings;
}
public string Code { get; }
public string Name { get; }
public IReadOnlyDictionary<string, string> Strings { get; }
}

View File

@@ -0,0 +1,46 @@
using System.Text.Json;
namespace ClaudeDo.Localization;
public static class LocaleJson
{
public static LocaleFile Parse(string json)
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var code = "";
var name = "";
if (root.TryGetProperty("metadata", out var meta) && meta.ValueKind == JsonValueKind.Object)
{
if (meta.TryGetProperty("code", out var c)) code = c.GetString() ?? "";
if (meta.TryGetProperty("name", out var n)) name = n.GetString() ?? "";
}
var strings = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var prop in root.EnumerateObject())
{
if (prop.NameEquals("metadata")) continue;
Flatten(prop.Name, prop.Value, strings);
}
return new LocaleFile(code, name, strings);
}
private static void Flatten(string prefix, JsonElement el, IDictionary<string, string> into)
{
switch (el.ValueKind)
{
case JsonValueKind.Object:
foreach (var p in el.EnumerateObject())
Flatten($"{prefix}.{p.Name}", p.Value, into);
break;
case JsonValueKind.String:
into[prefix] = el.GetString() ?? "";
break;
default:
into[prefix] = el.ToString();
break;
}
}
}

View File

@@ -0,0 +1,31 @@
namespace ClaudeDo.Localization;
public sealed class LocaleStore
{
private readonly Dictionary<string, LocaleFile> _byCode;
private LocaleStore(Dictionary<string, LocaleFile> byCode) => _byCode = byCode;
public IReadOnlyList<LocaleFile> Available => _byCode.Values.ToList();
public bool TryGet(string code, out LocaleFile? file) => _byCode.TryGetValue(code, out file);
public static LocaleStore Load(string folder)
{
var byCode = new Dictionary<string, LocaleFile>(StringComparer.OrdinalIgnoreCase);
if (Directory.Exists(folder))
{
foreach (var path in Directory.EnumerateFiles(folder, "*.json"))
{
try
{
var file = LocaleJson.Parse(File.ReadAllText(path));
if (!string.IsNullOrWhiteSpace(file.Code))
byCode[file.Code] = file;
}
catch { /* skip malformed locale files */ }
}
}
return new LocaleStore(byCode);
}
}

View File

@@ -0,0 +1,7 @@
<Project>
<ItemGroup>
<Content Include="$(MSBuildThisFileDirectory)locales\*.json"
Link="locales\%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,55 @@
namespace ClaudeDo.Localization;
public sealed class Localizer : ILocalizer
{
private readonly LocaleStore _store;
private readonly string _fallbackCode;
private LocaleFile? _active;
private LocaleFile? _fallback;
public Localizer(LocaleStore store, string code, string fallbackCode = "en")
{
_store = store;
_fallbackCode = fallbackCode;
_store.TryGet(fallbackCode, out _fallback);
SetLanguage(code);
}
public string CurrentCode { get; private set; } = "";
public IReadOnlyList<LanguageOption> AvailableLanguages =>
_store.Available.Select(f => new LanguageOption(f.Code, f.Name)).ToList();
public event EventHandler? LanguageChanged;
public string this[string key]
{
get
{
if (_active is not null && _active.Strings.TryGetValue(key, out var v)) return v;
if (_fallback is not null && _fallback.Strings.TryGetValue(key, out var fv)) return fv;
return key;
}
}
public string Get(string key, params object[] args)
{
var fmt = this[key];
return args.Length == 0 ? fmt : string.Format(fmt, args);
}
public void SetLanguage(string code)
{
if (_store.TryGet(code, out var f) && f is not null)
{
_active = f;
CurrentCode = f.Code;
}
else
{
_active = _fallback;
CurrentCode = _fallback?.Code ?? code;
}
LanguageChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,388 @@
{
"metadata": { "code": "de", "name": "Deutsch" },
"settings": {
"title": "EINSTELLUNGEN",
"save": "Speichern",
"cancel": "Abbrechen",
"language": "Sprache",
"tabGeneral": "Allgemein",
"tabWorktrees": "Worktrees",
"tabFiles": "Dateien",
"tabPrime": "Prime Claude",
"general": {
"defaultInstructions": "Standard-Anweisungen",
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
"model": "Modell",
"maxTurns": "Max. Durchläufe",
"permission": "Berechtigung",
"maxParallelExecutions": "Max. parallele Ausführungen",
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
"standupWeekday": "Standup-Wochentag",
"weekdaySunday": "Sonntag",
"weekdayMonday": "Montag",
"weekdayTuesday": "Dienstag",
"weekdayWednesday": "Mittwoch",
"weekdayThursday": "Donnerstag",
"weekdayFriday": "Freitag",
"weekdaySaturday": "Samstag"
},
"worktrees": {
"strategy": "Strategie",
"centralWorktreeRoot": "Zentrales Worktree-Verzeichnis",
"autoCleanup": "Abgeschlossene Worktrees automatisch aufräumen nach",
"days": "Tagen",
"cleanupFinished": "Abgeschlossene Worktrees aufräumen",
"forceRemoveAll": "Alle Worktrees zwangsweise entfernen",
"confirmRemoveAll": "ALLE Worktrees entfernen? Nicht committete Arbeit geht verloren.",
"removeAll": "Alle entfernen"
},
"files": {
"agentsSection": "AGENTEN",
"agentsHint": "Mitgelieferte Standard-Agenten wiederherstellen. Vorhandene Dateien werden nicht überschrieben.",
"restoreDefaultAgents": "Standard-Agenten wiederherstellen",
"promptsSection": "PROMPTS",
"systemPrompt": "System",
"planningPrompt": "Planung",
"agentPrompt": "Agent",
"openInEditor": "Im Editor öffnen"
},
"prime": {
"description": "Bereite dein Claude-Nutzungsfenster vor, indem an den von dir gewählten Tagen zu einer bestimmten Zeit ein einzelner nicht-interaktiver Ping ausgelöst wird. Läuft nur, solange ClaudeDo geöffnet ist. Wenn die App innerhalb von 30 Minuten vor der Zielzeit startet, wird der Ping sofort ausgelöst.",
"addSchedule": "+ Zeitplan hinzufügen",
"dayMo": "Mo",
"dayTu": "Di",
"dayWe": "Mi",
"dayTh": "Do",
"dayFr": "Fr",
"daySa": "Sa",
"daySu": "So"
}
},
"tasks": {
"sortTip": "Sortieren",
"showCompletedTip": "Abgeschlossene anzeigen",
"listSettingsTip": "Listeneinstellungen",
"addPlaceholder": "Aufgabe hinzufügen…",
"enterKey": "ENTER",
"notesPinnedRow": "Notizen (Tagesnotizen)",
"overdue": "ÜBERFÄLLIG",
"tasks": "AUFGABEN",
"clearCompletedTip": "Alle abgeschlossenen löschen",
"ctxSendToQueue": "In Warteschlange einreihen",
"ctxRemoveFromQueue": "Aus Warteschlange entfernen",
"ctxCancelExecution": "Ausführung abbrechen",
"ctxMarkAs": "Markieren als",
"ctxMarkDone": "Erledigt",
"ctxMarkCancelled": "Abgebrochen",
"ctxRunInteractively": "Interaktiv ausführen",
"ctxOpenPlanningSession": "Planungssitzung öffnen",
"ctxResumePlanningSession": "Planungssitzung fortsetzen",
"ctxDiscardPlanningSession": "Planungssitzung verwerfen",
"ctxQueueSubtasks": "Teilaufgaben nacheinander einreihen",
"ctxScheduleFor": "Planen für...",
"ctxClearSchedule": "Zeitplan entfernen",
"badgeDraft": "ENTWURF",
"badgePlanned": "GEPLANT",
"approve": "Genehmigen",
"approveTip": "Genehmigen — als Erledigt markieren",
"reject": "Ablehnen",
"rejectTip": "Mit Feedback ablehnen und erneut ausführen",
"park": "Parken",
"parkTip": "Zur manuellen Bearbeitung auf Leerlauf zurücksetzen",
"cancel": "Abbrechen",
"cancelTip": "Diese Aufgabe abbrechen",
"removeFromQueueTip": "Aus Warteschlange entfernen",
"scheduleTitle": "Aufgabe planen",
"scheduleWhen": "WANN",
"scheduleConfirm": "Planen",
"rejectRerunTitle": "Ablehnen & erneut ausführen",
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
"rerun": "Erneut ausführen"
},
"lists": {
"heading": "Listen",
"searchPlaceholder": "Aufgaben suchen…",
"searchKbd": "Strg K",
"settingsTip": "Einstellungen",
"smartListsLabel": "INTELLIGENTE LISTEN",
"myListsLabel": "MEINE LISTEN",
"contextSettings": "Einstellungen...",
"contextWorktrees": "Worktrees…",
"contextOpenExplorer": "Im Explorer öffnen",
"contextOpenTerminal": "Im Terminal öffnen",
"newList": "Neue Liste",
"addReposTip": "Repos als Listen hinzufügen"
},
"details": {
"deleteTaskTip": "Aufgabe löschen",
"closeTip": "Schließen",
"copyTaskIdTip": "Aufgaben-ID kopieren",
"starTip": "Favorit",
"agentSettingsTip": "Agent-Einstellungen",
"agentSettingsHeading": "Agent-Einstellungen (Überschreibungen)",
"modelLabel": "Modell",
"systemPromptLabel": "System-Prompt (angehängt)",
"agentFileLabel": "Agent-Datei",
"mergeLabel": "MERGE",
"mergeTargetLabel": "Merge-Ziel",
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
"stepsLabel": "SCHRITTE",
"addStepPlaceholder": "Schritt hinzufügen...",
"detailsLabel": "DETAILS",
"copyDescriptionTip": "Beschreibung in die Zwischenablage kopieren",
"toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten",
"previewBtn": "Vorschau",
"editBtn": "Bearbeiten",
"descriptionPlaceholder": "Aufgabendetails hinzufügen (Markdown unterstützt)..."
},
"agent": {
"stopTip": "Agent stoppen",
"sendToQueue": "In Warteschlange einreihen",
"sendToQueueTip": "Diese Aufgabe einreihen, damit der Worker sie übernimmt",
"removeFromQueue": "Aus Warteschlange entfernen",
"removeFromQueueTip": "Diese Aufgabe wieder aus der Warteschlange nehmen",
"worktreeLabel": "WORKTREE",
"copyPathTip": "Pfad kopieren",
"diffLabel": "DIFF",
"openDiff": "Diff öffnen",
"worktreeBtn": "Worktree",
"openWorktreeTip": "Worktree im Datei-Explorer öffnen",
"continue": "Fortsetzen",
"continueTip": "Die letzte Sitzung fortsetzen und weitermachen",
"resetAndRetry": "Zurücksetzen & erneut versuchen",
"resetAndRetryTip": "Den Worktree verwerfen und die Aufgabe erneut einreihen, um von vorn zu beginnen"
},
"notes": {
"today": "Heute",
"add": "Hinzufügen",
"newNotePlaceholder": "Neue Notiz…",
"save": "Speichern",
"delete": "Löschen"
},
"session": {
"chipLive": "LIVE",
"chipDone": "FERTIG",
"chipFailed": "FEHLGESCHLAGEN"
},
"modals": {
"about": {
"title": "ÜBER",
"version": "Version",
"data": "Daten",
"logs": "Logs",
"config": "Konfiguration",
"open": "Öffnen"
},
"workerConnection": {
"title": "WORKER NICHT ERREICHBAR",
"body": "ClaudeDo kann den Hintergrund-Worker nicht erreichen. Normalerweise wird er bei der Anmeldung automatisch gestartet. Du kannst ihn jetzt starten oder neu installieren, falls das Problem bestehen bleibt.",
"dismiss": "Ausblenden",
"rerunInstaller": "Installer erneut ausführen",
"startWorker": "Worker starten"
},
"listSettings": {
"title": "LISTENEINSTELLUNGEN",
"deleteList": "Liste löschen",
"sectionGeneral": "ALLGEMEIN",
"name": "Name",
"workingDirectory": "Arbeitsverzeichnis",
"workingDirectoryPlaceholder": "(keines)",
"browse": "Durchsuchen...",
"defaultCommitType": "Standard-Commit-Typ",
"sectionAgent": "AGENT",
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
"model": "Modell",
"systemPrompt": "System-Prompt (angehängt)",
"agentFile": "Agent-Datei"
},
"merge": {
"title": "WORKTREE MERGEN",
"windowTitle": "Worktree mergen",
"cancel": "Abbrechen",
"merge": "Mergen",
"targetBranch": "Ziel-Branch",
"removeWorktree": "Worktree nach dem Mergen entfernen",
"commitMessage": "Commit-Nachricht",
"conflictedFiles": "Konfliktdateien:"
},
"diff": {
"title": "DIFF",
"windowTitle": "Diff",
"merge": "Mergen…"
},
"worktree": {
"title": "Worktree"
},
"worktreesOverview": {
"refresh": "Aktualisieren",
"cleanupFinished": "Abgeschlossene aufräumen",
"columnTask": "AUFGABE",
"columnState": "STATUS",
"columnDiff": "DIFF",
"columnAge": "ALTER",
"phantom": "Phantom",
"phantomTooltip": "Verzeichnis fehlt auf der Festplatte",
"ctxShowDiff": "Diff anzeigen",
"ctxOpenInExplorer": "Im Explorer öffnen",
"ctxJumpToTask": "Zur Aufgabe springen",
"ctxMerge": "Mergen…",
"ctxDiscard": "Verwerfen",
"ctxKeep": "Behalten",
"ctxCopyBranch": "Branch kopieren",
"ctxCopyPath": "Pfad kopieren",
"ctxForceRemove": "Zwangsweise entfernen"
},
"repoImport": {
"title": "REPOS ALS LISTEN HINZUFÜGEN",
"windowTitle": "Repos als Listen hinzufügen",
"cancel": "Abbrechen",
"searchPlaceholder": "Repos suchen…",
"addFolder": "Ordner hinzufügen…",
"forgetFolders": "Ordner vergessen",
"alreadyAdded": "(bereits hinzugefügt)"
},
"unfinishedPlanning": {
"title": "UNVOLLENDETE PLANUNGSSITZUNG",
"windowTitle": "Unvollendete Planungssitzung",
"discard": "Verwerfen",
"finalize": "Abschließen",
"resume": "Fortsetzen",
"draftTasksSuffix": " Entwurfsaufgabe(n) warten auf Abschluss."
},
"weeklyReport": {
"title": "WOCHENBERICHT",
"windowTitle": "Wochenbericht",
"from": "Von",
"to": "Bis",
"generate": "Erstellen",
"regenerate": "Neu erstellen",
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
}
},
"installer": {
"nav": {
"back": "Zurück",
"next": "Weiter →",
"install": "Installieren",
"browse": "Durchsuchen...",
"cancel": "Abbrechen"
},
"welcome": {
"title": "Willkommen",
"heading": "ClaudeDo installieren",
"subheading": "Wähle aus, wohin ClaudeDo installiert werden soll, und klicke dann auf Weiter.",
"updateSubheading": "Deine Aufgaben, Konfiguration und Datenbank bleiben erhalten. Klicke auf Weiter, um fortzufahren.",
"installDirectory": "Installationsverzeichnis",
"registerMcp": "MCP-Server bei Claude registrieren",
"registerMcpHint": "Führt 'claude mcp add' aus, damit Claude deine ClaudeDo-Aufgaben sehen und verwalten kann. Du kannst dies später ändern."
},
"paths": {
"title": "Datenpfade",
"subtitle": "Lege fest, wo ClaudeDo seine Daten speichert.",
"databasePath": "Datenbankpfad",
"logDirectory": "Log-Verzeichnis",
"sandboxRoot": "Sandbox-Verzeichnis",
"worktreeStrategy": "Worktree-Strategie",
"centralWorktreeRoot": "Zentrales Worktree-Verzeichnis"
},
"service": {
"title": "Worker",
"subtitle": "Konfiguriere den ClaudeDo-Hintergrund-Worker.",
"signalRPort": "SignalR-Port",
"queueBackstopInterval": "Warteschlangen-Backstop-Intervall (ms)",
"claudeCliPath": "Claude-CLI-Pfad",
"autostart": "Worker bei der Anmeldung automatisch starten",
"autostartHint": "Der Worker läuft als du (der angemeldete Benutzer) über eine benutzerbezogene Anmelde-Aufgabe, sodass er deine Claude-CLI-Authentifizierung nutzen kann.",
"restartDelay": "Neustart-Verzögerung (ms)"
},
"uiSettings": {
"title": "UI-Einstellungen",
"subtitle": "Konfiguriere die Verbindungseinstellungen der ClaudeDo-Desktop-Oberfläche.",
"syncWithService": "Mit Worker-Einstellungen synchronisieren",
"signalRUrl": "SignalR-URL",
"syncHint": "Bei Synchronisierung werden diese Werte aus den Seiten „Worker“ und „Datenpfade“ abgeleitet."
},
"install": {
"title": "Installation",
"subtitle": "Klicke auf Installieren, um ClaudeDo zu erstellen und bereitzustellen.",
"launch": "ClaudeDo starten"
},
"settings": {
"removeUserData": "Benutzerdaten entfernen (Aufgaben, Logs, Konfigurationen in ~/.todo-app)",
"uninstall": "Deinstallieren",
"repair": "Reparieren",
"save": "Speichern",
"close": "Schließen"
},
"selfUpdate": {
"heading": "Ein neuerer Installer ist verfügbar",
"update": "Aktualisieren",
"continueAnyway": "Trotzdem fortfahren"
}
},
"planning": {
"conflict": {
"windowTitle": "Merge-Konflikt",
"modalTitle": "MERGE-KONFLIKT",
"openInVsCode": "Alle in VS Code öffnen",
"resolved": "Ich habe gelöst — fortfahren",
"abort": "Diesen Merge abbrechen"
},
"diff": {
"windowTitle": "Planung — Kombiniertes Diff",
"modalTitle": "PLANUNG — KOMBINIERTES DIFF",
"previewCombined": "Kombinierte Vorschau",
"loading": "Wird geladen…"
}
},
"controls": {
"datePicker": {
"today": "Heute",
"tomorrow": "Morgen",
"nextMon": "Nächster Mo",
"clear": "Löschen",
"time": "Zeit",
"done": "Fertig"
}
},
"shell": {
"menu": {
"help": "Hilfe",
"checkForUpdates": "Nach Updates suchen",
"restartWorker": "Worker neu starten",
"worktrees": "Worktrees…",
"weeklyReport": "Wochenbericht…",
"about": "Über…",
"addRepos": "Repos als Listen hinzufügen…"
},
"update": {
"available": "Update verfügbar: v",
"updateNow": "Jetzt aktualisieren",
"dismiss": "Ausblenden"
}
},
"vm": {
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
"shell": { "restartingWorker": "Worker wird neu gestartet…" },
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" },
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen." },
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien)." },
"merge": { "commitMessage": "Merge-Aufgabe: {0}", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "openFailed": "Öffnen fehlgeschlagen: {0}" },
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." },
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen." },
"listSettings": { "untitled": "Unbenannt" },
"details": { "effectiveIfInherited": "Effektiv bei Vererbung: {0}" },
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
}
}

View File

@@ -0,0 +1,388 @@
{
"metadata": { "code": "en", "name": "English" },
"settings": {
"title": "SETTINGS",
"save": "Save",
"cancel": "Cancel",
"language": "Language",
"tabGeneral": "General",
"tabWorktrees": "Worktrees",
"tabFiles": "Files",
"tabPrime": "Prime Claude",
"general": {
"defaultInstructions": "Default instructions",
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
"model": "Model",
"maxTurns": "Max turns",
"permission": "Permission",
"maxParallelExecutions": "Max parallel executions",
"maxParallelExecutionsHint": "How many queued tasks the worker runs at once.",
"reportExcludedPaths": "Report: excluded paths (one per line)",
"standupWeekday": "Standup weekday",
"weekdaySunday": "Sunday",
"weekdayMonday": "Monday",
"weekdayTuesday": "Tuesday",
"weekdayWednesday": "Wednesday",
"weekdayThursday": "Thursday",
"weekdayFriday": "Friday",
"weekdaySaturday": "Saturday"
},
"worktrees": {
"strategy": "Strategy",
"centralWorktreeRoot": "Central worktree root",
"autoCleanup": "Auto-cleanup finished worktrees after",
"days": "days",
"cleanupFinished": "Cleanup finished worktrees",
"forceRemoveAll": "Force-remove all worktrees",
"confirmRemoveAll": "Remove ALL worktrees? Uncommitted work will be lost.",
"removeAll": "Remove All"
},
"files": {
"agentsSection": "AGENTS",
"agentsHint": "Restore bundled default agents. Existing files are not overwritten.",
"restoreDefaultAgents": "Restore default agents",
"promptsSection": "PROMPTS",
"systemPrompt": "System",
"planningPrompt": "Planning",
"agentPrompt": "Agent",
"openInEditor": "Open in editor"
},
"prime": {
"description": "Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately.",
"addSchedule": "+ Add schedule",
"dayMo": "Mo",
"dayTu": "Tu",
"dayWe": "We",
"dayTh": "Th",
"dayFr": "Fr",
"daySa": "Sa",
"daySu": "Su"
}
},
"tasks": {
"sortTip": "Sort",
"showCompletedTip": "Show completed",
"listSettingsTip": "List settings",
"addPlaceholder": "Add a task…",
"enterKey": "ENTER",
"notesPinnedRow": "Notes (daily notes)",
"overdue": "OVERDUE",
"tasks": "TASKS",
"clearCompletedTip": "Clear all completed",
"ctxSendToQueue": "Send to queue",
"ctxRemoveFromQueue": "Remove from queue",
"ctxCancelExecution": "Cancel execution",
"ctxMarkAs": "Mark as",
"ctxMarkDone": "Done",
"ctxMarkCancelled": "Cancelled",
"ctxRunInteractively": "Run interactively",
"ctxOpenPlanningSession": "Open planning Session",
"ctxResumePlanningSession": "Resume planning Session",
"ctxDiscardPlanningSession": "Discard planning session",
"ctxQueueSubtasks": "Queue subtasks sequentially",
"ctxScheduleFor": "Schedule for...",
"ctxClearSchedule": "Clear schedule",
"badgeDraft": "DRAFT",
"badgePlanned": "PLANNED",
"approve": "Approve",
"approveTip": "Approve — mark Done",
"reject": "Reject",
"rejectTip": "Reject with feedback and re-run",
"park": "Park",
"parkTip": "Send back to Idle for manual editing",
"cancel": "Cancel",
"cancelTip": "Cancel this task",
"removeFromQueueTip": "Remove from queue",
"scheduleTitle": "Schedule task",
"scheduleWhen": "WHEN",
"scheduleConfirm": "Schedule",
"rejectRerunTitle": "Reject & re-run",
"feedbackLabel": "FEEDBACK FOR THE AGENT",
"feedbackPlaceholder": "What should the agent fix?",
"rerun": "Re-run"
},
"lists": {
"heading": "Lists",
"searchPlaceholder": "Search tasks…",
"searchKbd": "Ctrl K",
"settingsTip": "Settings",
"smartListsLabel": "SMART LISTS",
"myListsLabel": "MY LISTS",
"contextSettings": "Settings...",
"contextWorktrees": "Worktrees…",
"contextOpenExplorer": "Open in Explorer",
"contextOpenTerminal": "Open in Terminal",
"newList": "New list",
"addReposTip": "Add repos as lists"
},
"details": {
"deleteTaskTip": "Delete task",
"closeTip": "Close",
"copyTaskIdTip": "Copy task ID",
"starTip": "Star",
"agentSettingsTip": "Agent settings",
"agentSettingsHeading": "Agent settings (overrides)",
"modelLabel": "Model",
"systemPromptLabel": "System prompt (appended)",
"agentFileLabel": "Agent file",
"mergeLabel": "MERGE",
"mergeTargetLabel": "Merge target",
"reviewCombinedDiff": "Review combined diff",
"mergeAllSubtasks": "Merge all subtasks",
"stepsLabel": "STEPS",
"addStepPlaceholder": "Add a step...",
"detailsLabel": "DETAILS",
"copyDescriptionTip": "Copy description to clipboard",
"toggleEditPreviewTip": "Toggle edit/preview",
"previewBtn": "Preview",
"editBtn": "Edit",
"descriptionPlaceholder": "Add task details (markdown supported)..."
},
"agent": {
"stopTip": "Stop agent",
"sendToQueue": "Send to queue",
"sendToQueueTip": "Queue this task for the worker to pick up",
"removeFromQueue": "Remove from queue",
"removeFromQueueTip": "Take this task back out of the queue",
"worktreeLabel": "WORKTREE",
"copyPathTip": "Copy path",
"diffLabel": "DIFF",
"openDiff": "Open diff",
"worktreeBtn": "Worktree",
"openWorktreeTip": "Open worktree in file explorer",
"continue": "Continue",
"continueTip": "Resume the last session and keep going",
"resetAndRetry": "Reset & retry",
"resetAndRetryTip": "Discard the worktree and re-queue the task to run from scratch"
},
"notes": {
"today": "Today",
"add": "Add",
"newNotePlaceholder": "New note…",
"save": "Save",
"delete": "Delete"
},
"session": {
"chipLive": "LIVE",
"chipDone": "DONE",
"chipFailed": "FAILED"
},
"modals": {
"about": {
"title": "ABOUT",
"version": "Version",
"data": "Data",
"logs": "Logs",
"config": "Config",
"open": "Open"
},
"workerConnection": {
"title": "WORKER NOT REACHABLE",
"body": "ClaudeDo can't reach the background worker. It is normally started automatically at logon. You can start it now, or reinstall if the problem persists.",
"dismiss": "Dismiss",
"rerunInstaller": "Rerun Installer",
"startWorker": "Start Worker"
},
"listSettings": {
"title": "LIST SETTINGS",
"deleteList": "Delete list",
"sectionGeneral": "GENERAL",
"name": "Name",
"workingDirectory": "Working directory",
"workingDirectoryPlaceholder": "(none)",
"browse": "Browse...",
"defaultCommitType": "Default commit type",
"sectionAgent": "AGENT",
"resetAgentSettings": "Reset agent settings",
"model": "Model",
"systemPrompt": "System prompt (appended)",
"agentFile": "Agent file"
},
"merge": {
"title": "MERGE WORKTREE",
"windowTitle": "Merge worktree",
"cancel": "Cancel",
"merge": "Merge",
"targetBranch": "Target branch",
"removeWorktree": "Remove worktree after merge",
"commitMessage": "Commit message",
"conflictedFiles": "Conflicted files:"
},
"diff": {
"title": "DIFF",
"windowTitle": "Diff",
"merge": "Merge…"
},
"worktree": {
"title": "Worktree"
},
"worktreesOverview": {
"refresh": "Refresh",
"cleanupFinished": "Cleanup finished",
"columnTask": "TASK",
"columnState": "STATE",
"columnDiff": "DIFF",
"columnAge": "AGE",
"phantom": "phantom",
"phantomTooltip": "Directory missing on disk",
"ctxShowDiff": "Show diff",
"ctxOpenInExplorer": "Open in Explorer",
"ctxJumpToTask": "Jump to task",
"ctxMerge": "Merge…",
"ctxDiscard": "Discard",
"ctxKeep": "Keep",
"ctxCopyBranch": "Copy branch",
"ctxCopyPath": "Copy path",
"ctxForceRemove": "Force remove"
},
"repoImport": {
"title": "ADD REPOS AS LISTS",
"windowTitle": "Add repos as lists",
"cancel": "Cancel",
"searchPlaceholder": "Search repos…",
"addFolder": "Add folder…",
"forgetFolders": "Forget folders",
"alreadyAdded": "(already added)"
},
"unfinishedPlanning": {
"title": "UNFINISHED PLANNING SESSION",
"windowTitle": "Unfinished planning session",
"discard": "Discard",
"finalize": "Finalize",
"resume": "Resume",
"draftTasksSuffix": " draft task(s) waiting to be finalized."
},
"weeklyReport": {
"title": "WEEKLY REPORT",
"windowTitle": "Weekly Report",
"from": "From",
"to": "To",
"generate": "Generate",
"regenerate": "Regenerate",
"emptyStateHint": "No report for this range yet. Click “Generate”."
}
},
"installer": {
"nav": {
"back": "Back",
"next": "Next →",
"install": "Install",
"browse": "Browse...",
"cancel": "Cancel"
},
"welcome": {
"title": "Welcome",
"heading": "Install ClaudeDo",
"subheading": "Choose where to install ClaudeDo, then click Next.",
"updateSubheading": "Your tasks, config, and database will be preserved. Click Next to continue.",
"installDirectory": "Install Directory",
"registerMcp": "Register MCP server with Claude",
"registerMcpHint": "Runs 'claude mcp add' so Claude can view and manage your ClaudeDo tasks. You can change this later."
},
"paths": {
"title": "Data Paths",
"subtitle": "Configure where ClaudeDo stores its data.",
"databasePath": "Database Path",
"logDirectory": "Log Directory",
"sandboxRoot": "Sandbox Root",
"worktreeStrategy": "Worktree Strategy",
"centralWorktreeRoot": "Central Worktree Root"
},
"service": {
"title": "Worker",
"subtitle": "Configure the ClaudeDo background worker.",
"signalRPort": "SignalR Port",
"queueBackstopInterval": "Queue Backstop Interval (ms)",
"claudeCliPath": "Claude CLI Path",
"autostart": "Start worker automatically at logon",
"autostartHint": "The worker runs as you (the logged-in user) via a per-user logon task, so it can use your Claude CLI authentication.",
"restartDelay": "Restart Delay (ms)"
},
"uiSettings": {
"title": "UI Settings",
"subtitle": "Configure the ClaudeDo desktop UI connection settings.",
"syncWithService": "Sync with service settings",
"signalRUrl": "SignalR URL",
"syncHint": "When synced, these values are derived from the Service and Paths pages."
},
"install": {
"title": "Installation",
"subtitle": "Click Install to build and deploy ClaudeDo.",
"launch": "Launch ClaudeDo"
},
"settings": {
"removeUserData": "Remove user data (tasks, logs, configs in ~/.todo-app)",
"uninstall": "Uninstall",
"repair": "Repair",
"save": "Save",
"close": "Close"
},
"selfUpdate": {
"heading": "A newer installer is available",
"update": "Update",
"continueAnyway": "Continue anyway"
}
},
"planning": {
"conflict": {
"windowTitle": "Merge conflict",
"modalTitle": "MERGE CONFLICT",
"openInVsCode": "Open all in VS Code",
"resolved": "I've resolved — continue",
"abort": "Abort this merge"
},
"diff": {
"windowTitle": "Planning — Combined diff",
"modalTitle": "PLANNING — COMBINED DIFF",
"previewCombined": "Preview combined",
"loading": "Loading…"
}
},
"controls": {
"datePicker": {
"today": "Today",
"tomorrow": "Tomorrow",
"nextMon": "Next Mon",
"clear": "Clear",
"time": "Time",
"done": "Done"
}
},
"shell": {
"menu": {
"help": "Help",
"checkForUpdates": "Check for updates",
"restartWorker": "Restart worker",
"worktrees": "Worktrees…",
"weeklyReport": "Weekly Report…",
"about": "About…",
"addRepos": "Add repos as lists…"
},
"update": {
"available": "Update available: v",
"updateNow": "Update now",
"dismiss": "Dismiss"
}
},
"vm": {
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
"shell": { "restartingWorker": "Restarting worker…" },
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" },
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show." },
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." },
"merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" },
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." },
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed." },
"listSettings": { "untitled": "Untitled" },
"details": { "effectiveIfInherited": "Effective if inherited: {0}" },
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
}
}

View File

@@ -7,6 +7,7 @@ public sealed class AppSettings
{
public string DbPath { get; set; } = "~/.todo-app/todo.db";
public string SignalRUrl { get; set; } = "http://127.0.0.1:47821/hub";
public string Language { get; set; } = "";
private static readonly string ConfigPath = Paths.Expand("~/.todo-app/ui.config.json");
@@ -27,4 +28,12 @@ public sealed class AppSettings
}
return new();
}
public void Save()
{
var dir = Path.GetDirectoryName(ConfigPath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(ConfigPath, json);
}
}

View File

@@ -19,7 +19,9 @@ MVVM with CommunityToolkit.Mvvm source generators:
- **StatusBarView** — Connection status indicator, active task display
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath; also deletes the list (and its tasks) via a confirmed "Delete list" button. Opened via context menu or gear button on a list row.
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running.
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running. When notes mode is active (`IsNotesMode`), it hosts **NotesEditorView** instead of the task detail.
- **WeeklyReportModalView** — opened from Help menu ("Wochenbericht…"); date-range pickers default to "since last standup weekday → today"; Generate/Regenerate button; renders markdown via MarkdownView; reports are cached per range.
- **NotesEditorView** — day navigator (prev/next/date-picker/Today), bullet add/edit/delete for daily notes.
All views use compiled bindings (`x:DataType`).
@@ -31,10 +33,15 @@ All views use compiled bindings (`x:DataType`).
- **TaskItemViewModel** / **ListItemViewModel** — lightweight display VMs
- **TaskEditorViewModel** / **ListEditorViewModel** — dialog VMs with validation
- **StatusBarViewModel** — connection state and active tasks
- **WeeklyReportModalViewModel** — drives the weekly report modal
- **NotesEditorViewModel** — manages daily note bullet CRUD for the selected day
- **DetailsIslandViewModel** gains `IsNotesMode`, `ShowNotes()`, and hosts `NotesEditorViewModel`
- **TasksIslandViewModel** gains a pinned "Notes" pseudo-row (`ShowNotesRow`, `OpenNotesCommand`, `NotesRequested` event) that switches the Details island to notes mode
## Services
- **WorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync, ContinueTaskAsync, ResetTaskAsync, GetAgentsAsync, UpdateAppSettingsAsync, UpdateListAsync, UpdateListConfigAsync, GetListConfigAsync, UpdateTaskAgentSettingsAsync. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated, ListUpdated
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync, ContinueTaskAsync, ResetTaskAsync, GetAgentsAsync, UpdateAppSettingsAsync, UpdateListAsync, UpdateListConfigAsync, GetListConfigAsync, UpdateTaskAgentSettingsAsync, `GetWeekReportAsync`, `GenerateWeekReportAsync`, `GetDailyNotesAsync`, `AddDailyNoteAsync`, `UpdateDailyNoteAsync`, `DeleteDailyNoteAsync`. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated, ListUpdated
- **INotesApi** / **WorkerNotesApi** — thin wrapper over the daily-note WorkerClient methods (mirrors `IPrimeScheduleApi`). UI DTO: `DailyNoteDto(Id, Date, Text, SortOrder)`.
## Converters

View File

@@ -2,6 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
<ProjectReference Include="..\ClaudeDo.Releases\ClaudeDo.Releases.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using ClaudeDo.Localization;
namespace ClaudeDo.Ui.Localization;
/// Ambient access to the active localizer for code-built (ViewModel) strings.
/// Set once at startup. Defaults to a key-echo localizer so unit tests that
/// construct ViewModels without startup wiring do not crash.
public static class Loc
{
private static ILocalizer _current = new KeyEchoLocalizer();
public static ILocalizer Current
{
get => _current;
set
{
if (_current is not null) _current.LanguageChanged -= OnInnerChanged;
_current = value;
_current.LanguageChanged += OnInnerChanged;
OnInnerChanged(value, EventArgs.Empty);
}
}
public static event EventHandler? LanguageChanged;
private static void OnInnerChanged(object? sender, EventArgs e) =>
LanguageChanged?.Invoke(sender, e);
public static string T(string key) => Current[key];
public static string T(string key, params object[] args) => Current.Get(key, args);
private sealed class KeyEchoLocalizer : ILocalizer
{
public string this[string key] => key;
public string Get(string key, params object[] args) => key;
public string CurrentCode => "en";
public IReadOnlyList<LanguageOption> AvailableLanguages => Array.Empty<LanguageOption>();
public void SetLanguage(string code) { }
public event EventHandler? LanguageChanged { add { } remove { } }
}
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel;
using ClaudeDo.Localization;
namespace ClaudeDo.Ui.Localization;
public sealed class LocalizedString : INotifyPropertyChanged
{
private readonly ILocalizer _localizer;
private readonly string _key;
public LocalizedString(ILocalizer localizer, string key)
{
_localizer = localizer;
_key = key;
_localizer.LanguageChanged += (_, _) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
public string Value => _localizer[_key];
public event PropertyChangedEventHandler? PropertyChanged;
}

View File

@@ -0,0 +1,25 @@
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using ClaudeDo.Localization;
namespace ClaudeDo.Ui.Localization;
public sealed class TrExtension : MarkupExtension
{
public TrExtension() { }
public TrExtension(string key) => Key = key;
public string Key { get; set; } = "";
public static ILocalizer? Localizer { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var loc = Localizer ?? throw new InvalidOperationException("TrExtension.Localizer not initialized");
return new Binding(nameof(LocalizedString.Value))
{
Source = new LocalizedString(loc, Key),
Mode = BindingMode.OneWay
};
}
}

View File

@@ -0,0 +1,11 @@
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.Services.Interfaces;
public interface INotesApi
{
Task<List<DailyNoteDto>> ListAsync(DateOnly day);
Task<DailyNoteDto?> AddAsync(DateOnly day, string text);
Task UpdateAsync(string id, string text);
Task DeleteAsync(string id);
}

View File

@@ -50,4 +50,11 @@ public interface IWorkerClient : INotifyPropertyChanged
Task ContinuePlanningMergeAsync(string planningTaskId);
Task AbortPlanningMergeAsync(string planningTaskId);
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
Task<AppSettingsDto?> GetAppSettingsAsync();
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
Task UpdateDailyNoteAsync(string id, string text);
Task DeleteDailyNoteAsync(string id);
}

View File

@@ -0,0 +1,3 @@
namespace ClaudeDo.Ui.Services;
public sealed record DailyNoteDto(string Id, string Date, string Text, int SortOrder);

View File

@@ -326,6 +326,26 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
catch { /* offline */ }
}
private static string IsoDay(DateOnly d) => d.ToString("yyyy-MM-dd");
public Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end)
=> TryInvokeAsync<string>("GetWeekReport", IsoDay(start), IsoDay(end));
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
=> _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end));
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
=> TryInvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
public async Task UpdateDailyNoteAsync(string id, string text)
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
public async Task DeleteDailyNoteAsync(string id)
=> await _hub.InvokeAsync("DeleteDailyNote", id);
public async Task UpdateListAsync(UpdateListDto dto)
{
await _hub.InvokeAsync("UpdateList", dto);
@@ -474,7 +494,9 @@ public sealed record AppSettingsDto(
string WorktreeStrategy,
string? CentralWorktreeRoot,
bool WorktreeAutoCleanupEnabled,
int WorktreeAutoCleanupDays);
int WorktreeAutoCleanupDays,
string? ReportExcludedPaths,
int StandupWeekday);
public sealed record WorktreeCleanupDto(int Removed);
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);

View File

@@ -0,0 +1,13 @@
using ClaudeDo.Ui.Services.Interfaces;
namespace ClaudeDo.Ui.Services;
public sealed class WorkerNotesApi : INotesApi
{
private readonly WorkerClient _client;
public WorkerNotesApi(WorkerClient client) => _client = client;
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) => _client.GetDailyNotesAsync(day);
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) => _client.AddDailyNoteAsync(day, text);
public Task UpdateAsync(string id, string text) => _client.UpdateDailyNoteAsync(id, text);
public Task DeleteAsync(string id) => _client.DeleteDailyNoteAsync(id);
}

View File

@@ -6,7 +6,9 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -49,6 +51,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IWorkerClient _worker;
private readonly IServiceProvider _services;
private readonly INotesApi _notesApi;
[ObservableProperty] private bool _isNotesMode;
public NotesEditorViewModel Notes { get; private set; } = null!;
// Current task row (set by IslandsShellViewModel via Bind)
[ObservableProperty]
@@ -94,13 +100,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
private string _agentStatusLabel = "Idle";
public bool IsIdle => AgentStatusLabel == "Idle";
public bool IsQueued => AgentStatusLabel == "Queued";
public bool IsRunning => AgentStatusLabel == "Running";
public bool IsDone => AgentStatusLabel == "Done";
public bool IsFailed => AgentStatusLabel == "Failed";
public bool IsCancelled => AgentStatusLabel == "Cancelled";
private string _agentState = "idle";
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
public bool IsIdle => AgentState == "idle";
public bool IsQueued => AgentState == "queued";
public bool IsRunning => AgentState == "running";
public bool IsDone => AgentState == "done";
public bool IsFailed => AgentState == "failed";
public bool IsCancelled => AgentState == "cancelled";
// Recovery actions: Continue (resume session) for Failed/Cancelled.
public bool ShowContinue => IsFailed || IsCancelled;
@@ -111,8 +118,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
private string? _latestRunSessionId;
partial void OnAgentStatusLabelChanged(string value)
partial void OnAgentStateChanged(string value)
{
OnPropertyChanged(nameof(AgentStatusLabel));
OnPropertyChanged(nameof(IsIdle));
OnPropertyChanged(nameof(IsQueued));
OnPropertyChanged(nameof(IsRunning));
@@ -122,6 +130,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowContinue));
OnPropertyChanged(nameof(ShowResetAndRetry));
OnPropertyChanged(nameof(IsAgentSectionEnabled));
OnPropertyChanged(nameof(EffectiveModelLabel));
OnPropertyChanged(nameof(EffectiveAgentLabel));
}
[ObservableProperty] private string? _model;
@@ -134,6 +144,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[ObservableProperty] private string _effectiveSystemPromptHint = "";
[ObservableProperty] private string _effectiveAgentHint = "";
public string EffectiveModelLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveModelHint);
public string EffectiveAgentLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveAgentHint);
partial void OnEffectiveModelHintChanged(string value) => OnPropertyChanged(nameof(EffectiveModelLabel));
partial void OnEffectiveAgentHintChanged(string value) => OnPropertyChanged(nameof(EffectiveAgentLabel));
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(
new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases));
@@ -218,6 +234,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Set by the view so DeleteTaskCommand can show an error message
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
{
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
ClaudeDo.Data.Models.TaskStatus.Running => "running",
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "running",
ClaudeDo.Data.Models.TaskStatus.Done => "done",
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
_ => "idle",
};
private static string FinishedStatusToStateKey(string status) => status switch
{
"done" => "done",
"failed" => "failed",
"cancelled" => "cancelled",
"waiting_for_review" => "running",
_ => status.ToLowerInvariant(),
};
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
{
try
@@ -228,16 +264,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
.FirstOrDefaultAsync(t => t.Id == taskId);
if (entity is null || Task?.Id != taskId) return;
AgentStatusLabel = entity.Status.ToString();
AgentState = StatusToStateKey(entity.Status);
}
catch { }
}
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services)
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services, INotesApi notesApi)
{
_dbFactory = dbFactory;
_worker = worker;
_services = services;
_notesApi = notesApi;
Notes = new NotesEditorViewModel(_notesApi);
Loc.LanguageChanged += (_, _) =>
{
OnPropertyChanged(nameof(AgentStatusLabel));
OnPropertyChanged(nameof(EffectiveModelLabel));
OnPropertyChanged(nameof(EffectiveAgentLabel));
};
// Subscribe once; filter by current task id inside the handler
_worker.TaskMessageEvent += OnTaskMessage;
@@ -257,7 +301,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// If the task row's live status changes (e.g. TaskStarted/Finished), mirror it.
_worker.TaskStartedEvent += (slot, taskId, startedAt) =>
{
if (Task?.Id == taskId) AgentStatusLabel = "Running";
if (Task?.Id == taskId) AgentState = "running";
};
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
{
@@ -268,7 +312,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
Kind = LogKind.Done,
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
});
AgentStatusLabel = status;
AgentState = FinishedStatusToStateKey(status);
// Re-query to pick up worktree created during the run.
_ = RefreshWorktreeAsync(taskId);
};
@@ -431,8 +475,16 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
}
}
public void ShowNotes()
{
Bind(null);
IsNotesMode = true;
_ = Notes.LoadDayAsync(DateOnly.FromDateTime(DateTime.Today));
}
public void Bind(TaskRowViewModel? row)
{
IsNotesMode = false;
_loadCts?.Cancel();
_loadCts?.Dispose();
_loadCts = new CancellationTokenSource();
@@ -458,7 +510,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreePath = null;
WorktreeStateLabel = null;
BranchLine = null;
AgentStatusLabel = "Idle";
AgentState = "idle";
LatestRunSessionId = null;
_suppressAgentSave = true;
try
@@ -504,7 +556,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
AgentStatusLabel = entity.Status.ToString();
AgentState = StatusToStateKey(entity.Status);
await LoadAgentSettingsAsync(entity, ct);
ct.ThrowIfCancellationRequested();
@@ -715,7 +767,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
AgentStatusLabel = entity.Status.ToString();
AgentState = StatusToStateKey(entity.Status);
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
row.DiffStat = stat;
}
@@ -793,7 +845,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
? ClaudeDo.Data.Models.TaskStatus.Done
: ClaudeDo.Data.Models.TaskStatus.Idle;
Task.Status = entity.Status;
AgentStatusLabel = entity.Status.ToString();
AgentState = StatusToStateKey(entity.Status);
await repo.UpdateAsync(entity);
}
@@ -907,7 +959,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try
{
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued";
AgentState = "queued";
}
catch { /* offline */ }
}
@@ -923,7 +975,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try
{
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle);
AgentStatusLabel = "Idle";
AgentState = "idle";
}
catch { /* offline */ }
}
@@ -958,7 +1010,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try
{
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued";
AgentState = "queued";
}
catch { /* offline */ }
}

View File

@@ -6,6 +6,7 @@ using ClaudeDo.Data.Filtering;
using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
@@ -137,6 +138,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
public string UserName { get; } = Environment.UserName;
public string MachineName { get; } = Environment.MachineName;
public string MachineNameLocal => Loc.T("vm.lists.localSuffix", MachineName);
public string UserInitials { get; }
public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IServiceProvider? services = null, WorkerClient? worker = null)
@@ -170,12 +172,12 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
var smart = new[]
{
new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" },
new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" },
new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" },
new ListNavItemViewModel { Id = "virtual:queued", Name = "Queue", Kind = ListKind.Virtual, IconKey = "Inbox" },
new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" },
new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" },
new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "Sun" },
new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "Star" },
new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "Calendar" },
new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "Inbox" },
new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "Activity" },
new ListNavItemViewModel { Id = "virtual:review", Name = Loc.T("vm.lists.virtualReview"), Kind = ListKind.Virtual, IconKey = "Eye" },
};
foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); }
@@ -242,7 +244,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
var entity = new ListEntity
{
Id = Guid.NewGuid().ToString("N"),
Name = "New list",
Name = Loc.T("vm.lists.newList"),
DefaultCommitType = CommitTypeRegistry.DefaultType,
CreatedAt = DateTime.UtcNow,
};

View File

@@ -0,0 +1,83 @@
using System.Collections.ObjectModel;
using ClaudeDo.Ui.Services.Interfaces;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class NoteBulletViewModel : ViewModelBase
{
private readonly Func<NoteBulletViewModel, Task> _save;
private readonly Func<NoteBulletViewModel, Task> _delete;
public string Id { get; }
[ObservableProperty] private string _text;
public NoteBulletViewModel(string id, string text,
Func<NoteBulletViewModel, Task> save, Func<NoteBulletViewModel, Task> delete)
{
Id = id;
_text = text;
_save = save;
_delete = delete;
}
[RelayCommand] private Task Save() => _save(this);
[RelayCommand] private Task Delete() => _delete(this);
}
public sealed partial class NotesEditorViewModel : ViewModelBase
{
private readonly INotesApi _api;
public NotesEditorViewModel(INotesApi api) => _api = api;
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
[ObservableProperty] private string _newBulletText = "";
public DateTime CurrentDate
{
get => CurrentDay.ToDateTime(TimeOnly.MinValue);
set { var d = DateOnly.FromDateTime(value); if (d != CurrentDay) _ = LoadDayAsync(d); }
}
public string CurrentDayLabel => CurrentDay.ToString("dddd, dd.MM.yyyy");
public async Task LoadDayAsync(DateOnly day)
{
CurrentDay = day;
OnPropertyChanged(nameof(CurrentDate));
OnPropertyChanged(nameof(CurrentDayLabel));
Bullets.Clear();
foreach (var dto in await _api.ListAsync(day))
Bullets.Add(MakeBullet(dto.Id, dto.Text));
}
private NoteBulletViewModel MakeBullet(string id, string text) =>
new(id, text, SaveBulletAsync, DeleteBulletAsync);
[RelayCommand]
private async Task AddBullet()
{
var text = NewBulletText.Trim();
if (text.Length == 0) return;
var dto = await _api.AddAsync(CurrentDay, text);
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
NewBulletText = "";
}
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
[RelayCommand] private Task NextDay() => LoadDayAsync(CurrentDay.AddDays(1));
[RelayCommand] private Task Today() => LoadDayAsync(DateOnly.FromDateTime(DateTime.Today));
private Task SaveBulletAsync(NoteBulletViewModel b) => _api.UpdateAsync(b.Id, b.Text);
private async Task DeleteBulletAsync(NoteBulletViewModel b)
{
await _api.DeleteAsync(b.Id);
Bullets.Remove(b);
}
}

View File

@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -31,7 +32,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private bool _parentFinalized;
public DateTime CreatedAt { get; init; }
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
public int StepsCount { get; init; }
public int StepsCompleted { get; init; }
@@ -50,8 +51,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public string? PlanningBadge => PlanningPhase switch
{
PlanningPhase.Active => "PLANNING",
PlanningPhase.Finalized => "PLANNED",
PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
PlanningPhase.Finalized => Loc.T("vm.planningBadge.finalized"),
_ => null,
};
@@ -77,9 +78,19 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public string DiffAdditionsText => $"+{DiffAdditions}";
public string DiffDeletionsText => $"{DiffDeletions}";
public string StepsText => $"{StepsCompleted}/{StepsCount} steps";
public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount);
public string StatusLabel => Status == TaskStatus.WaitingForReview ? "Waiting for Review" : Status.ToString();
public string StatusLabel => Status switch
{
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
TaskStatus.Running => Loc.T("vm.taskStatus.running"),
TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"),
TaskStatus.Done => Loc.T("vm.taskStatus.done"),
TaskStatus.Failed => Loc.T("vm.taskStatus.failed"),
TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"),
_ => Status.ToString(),
};
public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
{
@@ -164,6 +175,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
public void RefreshLocalized()
{
OnPropertyChanged(nameof(StatusLabel));
OnPropertyChanged(nameof(PlanningBadge));
OnPropertyChanged(nameof(CreatedAtFormatted));
OnPropertyChanged(nameof(StepsText));
}
public static TaskRowViewModel FromEntity(TaskEntity t)
{
var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt };

View File

@@ -6,6 +6,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Filtering;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
@@ -25,8 +26,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
public event EventHandler? SelectionChanged;
public event EventHandler? FocusAddTaskRequested;
public event EventHandler? TasksChanged;
public event Action? NotesRequested;
public void RequestFocusAddTask() => FocusAddTaskRequested?.Invoke(this, EventArgs.Empty);
[RelayCommand]
private void OpenNotes()
{
SelectedTask = null;
NotesRequested?.Invoke();
}
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
@@ -44,7 +53,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
[ObservableProperty] private bool _hasOpen;
[ObservableProperty] private bool _hasCompleted;
[ObservableProperty] private bool _showOpenLabel;
[ObservableProperty] private string _completedHeader = "COMPLETED";
[ObservableProperty] private string _completedHeader = "";
[ObservableProperty] private bool _showNotesRow;
public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; }
@@ -52,6 +62,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
{
_dbFactory = dbFactory;
_worker = worker;
CompletedHeader = Loc.T("vm.tasksIsland.completedHeader");
if (_worker is not null)
{
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
@@ -59,6 +70,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
_worker.ListUpdatedEvent += OnWorkerListUpdated;
_worker.ConnectionRestoredEvent += () => LoadForList(_currentList);
}
Loc.LanguageChanged += (_, _) => RefreshLocalizedText();
}
private void RefreshLocalizedText()
{
CompletedHeader = Loc.T("vm.tasksIsland.completedHeader");
foreach (var row in Items) row.RefreshLocalized();
foreach (var row in CompletedItems) row.RefreshLocalized();
}
private async void OnWorkerListUpdated(string listId)
@@ -176,10 +195,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
HasOpen = false;
HasCompleted = false;
ShowOpenLabel = false;
ShowNotesRow = false;
if (list is null) return;
HeaderTitle = list.Name;
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
ShowNotesRow = list.Id == "smart:my-day";
_ = LoadForListAsync(list, ct);
}
@@ -329,7 +350,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
HasOpen = OpenItems.Count > 0;
HasCompleted = CompletedItems.Count > 0;
ShowOpenLabel = HasOpen && HasOverdue;
CompletedHeader = $"COMPLETED · {CompletedItems.Count}";
CompletedHeader = Loc.T("vm.tasksIsland.completedHeaderCount", CompletedItems.Count);
}
private void UpdateSubtitle()

View File

@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -23,9 +24,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
public UpdateCheckService UpdateCheck => _updateCheck;
public string ConnectionText =>
Worker?.IsConnected == true ? "Online"
: Worker?.IsReconnecting == true ? "Connecting"
: "Offline";
Worker?.IsConnected == true ? Loc.T("vm.connection.online")
: Worker?.IsReconnecting == true ? Loc.T("vm.connection.connecting")
: Loc.T("vm.connection.offline");
public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true;
@@ -34,6 +35,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
private readonly WorkerLocator _workerLocator = null!;
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
@@ -51,6 +53,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
// Set by MainWindow to open the global worktrees overview dialog.
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; }
// Set by MainWindow to open the weekly report dialog.
public Func<WeeklyReportModalViewModel, Task>? ShowWeeklyReportModal { get; set; }
// Set by MainWindow to open the worker-connection help dialog.
public Func<WorkerConnectionModalViewModel, Task>? ShowWorkerConnectionModal { get; set; }
@@ -178,6 +183,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
WorkerLocator workerLocator,
IDbContextFactory<ClaudeDoDbContext> dbFactory,
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
Func<MergeModalViewModel> mergeVmFactory,
Func<RepoImportModalViewModel> repoImportVmFactory)
{
@@ -187,10 +193,12 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
_workerLocator = workerLocator;
_dbFactory = dbFactory;
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
_weeklyReportVmFactory = weeklyReportVmFactory;
_mergeVmFactory = mergeVmFactory;
_repoImportVmFactory = repoImportVmFactory;
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
Tasks.NotesRequested += () => Details.ShowNotes();
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
Tasks.OpenListSettingsRequested += (_, _) =>
{
@@ -324,6 +332,22 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
finally { _worktreesOverviewOpen = false; }
}
private bool _weeklyReportOpen;
[RelayCommand]
private async Task OpenWeeklyReport()
{
if (ShowWeeklyReportModal is null || _weeklyReportOpen) return;
_weeklyReportOpen = true;
try
{
var vm = _weeklyReportVmFactory();
await vm.InitializeAsync();
await ShowWeeklyReportModal(vm);
}
finally { _weeklyReportOpen = false; }
}
[RelayCommand]
private async Task CheckForUpdatesAsync()
{
@@ -335,7 +359,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
[RelayCommand]
private async Task RestartWorkerAsync()
{
RestartWorkerStatus = "Restarting worker";
RestartWorkerStatus = Loc.T("vm.shell.restartingWorker");
try
{
await Task.Run(RestartWorkerService);

View File

@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Git;
using ClaudeDo.Ui.Localization;
namespace ClaudeDo.Ui.ViewModels.Modals;
@@ -91,13 +92,13 @@ public sealed partial class DiffModalViewModel : ViewModelBase
}
catch (Exception ex)
{
StatusMessage = $"Failed to load diff: {ex.Message}";
StatusMessage = Loc.T("vm.diff.loadFailed", ex.Message);
return;
}
if (string.IsNullOrWhiteSpace(raw))
{
StatusMessage = "No changes to show.";
StatusMessage = Loc.T("vm.diff.noChanges");
return;
}
@@ -169,7 +170,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase
}
SelectedFile = Files.Count > 0 ? Files[0] : null;
if (Files.Count == 0) StatusMessage = "No changes to show.";
if (Files.Count == 0) StatusMessage = Loc.T("vm.diff.noChanges");
}
private static void ParseHunkHeader(string header, out int oldStart, out int newStart)

View File

@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -80,7 +81,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
await _worker.UpdateListAsync(new UpdateListDto(
ListId,
string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name,
string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
DefaultCommitType));
@@ -93,7 +94,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
[RelayCommand]
private async Task DeleteAsync()
{
var displayName = string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name;
var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
if (ConfirmAsync is not null)
{
var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone.");

View File

@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -36,7 +37,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
{
TaskId = taskId;
TaskTitle = taskTitle;
CommitMessage = $"Merge task: {taskTitle}";
CommitMessage = Loc.T("vm.merge.commitMessage", taskTitle);
IsBusy = true;
try
@@ -45,7 +46,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
Branches.Clear();
if (targets is null)
{
ErrorMessage = "Worker offline — cannot list branches.";
ErrorMessage = Loc.T("vm.merge.workerOfflineBranches");
return;
}
foreach (var b in targets.LocalBranches) Branches.Add(b);
@@ -55,7 +56,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
}
catch (Exception ex)
{
ErrorMessage = $"Failed to load branches: {ex.Message}";
ErrorMessage = Loc.T("vm.merge.loadBranchesFailed", ex.Message);
}
finally { IsBusy = false; }
}
@@ -81,7 +82,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
case "merged":
SuccessMessage = result.ErrorMessage is not null
? $"Merged with warning: {result.ErrorMessage}"
: "Merged.";
: Loc.T("vm.merge.merged");
// Auto-close after a short delay.
_ = Task.Run(async () =>
{
@@ -92,19 +93,19 @@ public sealed partial class MergeModalViewModel : ViewModelBase
case "conflict":
HasConflict = true;
ConflictFiles = result.ConflictFiles;
ErrorMessage = "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.";
ErrorMessage = Loc.T("vm.merge.conflict");
break;
case "blocked":
ErrorMessage = $"Blocked: {result.ErrorMessage}";
ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? "");
break;
default:
ErrorMessage = $"Unknown status: {result.Status}";
ErrorMessage = Loc.T("vm.merge.unknownStatus", result.Status);
break;
}
}
catch (Exception ex)
{
ErrorMessage = $"Merge failed: {ex.Message}";
ErrorMessage = Loc.T("vm.merge.mergeFailed", ex.Message);
}
finally
{

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -26,13 +27,13 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
try
{
var r = await _worker.RestoreDefaultAgentsAsync();
if (r is null) StatusMessage = "Worker offline.";
else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = "No default agents bundled.";
else if (r.Copied == 0) StatusMessage = "All default agents already present.";
else StatusMessage = $"Restored {r.Copied} default agent(s).";
if (r is null) StatusMessage = Loc.T("vm.filesTab.workerOffline");
else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = Loc.T("vm.filesTab.noneBundled");
else if (r.Copied == 0) StatusMessage = Loc.T("vm.filesTab.allPresent");
else StatusMessage = Loc.T("vm.filesTab.restored", r.Copied);
await _worker.RefreshAgentsAsync();
}
catch (Exception ex) { StatusMessage = $"Restore failed: {ex.Message}"; }
catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.restoreFailed", ex.Message); }
finally { IsBusy = false; }
}
@@ -46,6 +47,6 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
var path = PromptFiles.PathFor(kind);
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
}
catch (Exception ex) { StatusMessage = $"Open failed: {ex.Message}"; }
catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.openFailed", ex.Message); }
}
}

View File

@@ -1,19 +1,48 @@
using ClaudeDo.Data.Models;
using ClaudeDo.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
{
private readonly ILocalizer? _localizer;
private readonly Action<string>? _persist;
[ObservableProperty] private string _defaultClaudeInstructions = "";
[ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias;
[ObservableProperty] private int _defaultMaxTurns = 100;
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
[ObservableProperty] private int _maxParallelExecutions = 1;
// Newline-separated path prefixes excluded from the weekly report.
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
[ObservableProperty] private int _standupWeekday = (int)DayOfWeek.Wednesday;
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
public GeneralSettingsTabViewModel() { }
public GeneralSettingsTabViewModel(ILocalizer localizer, Action<string> persist)
{
_localizer = localizer;
_persist = persist;
Languages = localizer.AvailableLanguages;
_selectedLanguage = Languages.FirstOrDefault(l => l.Code == localizer.CurrentCode);
}
public IReadOnlyList<LanguageOption> Languages { get; } = Array.Empty<LanguageOption>();
[ObservableProperty] private LanguageOption? _selectedLanguage;
partial void OnSelectedLanguageChanged(LanguageOption? value)
{
if (value is null || _localizer is null) return;
_localizer.SetLanguage(value.Value.Code);
_persist?.Invoke(value.Value.Code);
}
public string? Validate()
{
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)

View File

@@ -1,4 +1,5 @@
using System.IO;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -43,7 +44,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
try
{
var r = await _worker.CleanupFinishedWorktreesAsync();
StatusMessage = r is null ? "Worker offline." : $"Removed {r.Removed} worktree(s).";
StatusMessage = r is null ? Loc.T("vm.worktreesTab.workerOffline") : Loc.T("vm.worktreesTab.removed", r.Removed);
}
finally { IsBusy = false; }
}
@@ -58,9 +59,9 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
try
{
var r = await _worker.ResetAllWorktreesAsync();
if (r is null) StatusMessage = "Worker offline.";
else if (r.Blocked) StatusMessage = $"Cannot force-remove: {r.RunningTasks} task(s) still running. Cancel them first.";
else StatusMessage = $"Removed {r.Removed} worktree(s) from {r.TasksAffected} task(s).";
if (r is null) StatusMessage = Loc.T("vm.worktreesTab.workerOffline");
else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks);
else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected);
}
finally { IsBusy = false; }
}

View File

@@ -1,4 +1,7 @@
using System.Linq;
using ClaudeDo.Data;
using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -21,10 +24,15 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
public Action? CloseAction { get; set; }
public SettingsModalViewModel(WorkerClient worker, PrimeClaudeTabViewModel prime)
public SettingsModalViewModel(WorkerClient worker, PrimeClaudeTabViewModel prime,
ILocalizer localizer, AppSettings appSettings)
{
_worker = worker;
General = new GeneralSettingsTabViewModel();
General = new GeneralSettingsTabViewModel(localizer, code =>
{
appSettings.Language = code;
appSettings.Save();
});
Worktrees = new WorktreesSettingsTabViewModel(worker);
Files = new FilesSettingsTabViewModel(worker);
Prime = prime;
@@ -47,8 +55,13 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
Worktrees.CentralWorktreeRoot = dto.CentralWorktreeRoot;
Worktrees.WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
Worktrees.WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays;
General.ReportExcludedPaths = string.IsNullOrWhiteSpace(dto.ReportExcludedPaths)
? @"C:\Private"
: string.Join(Environment.NewLine,
System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new());
General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday;
}
else StatusMessage = "Worker offline — settings read-only.";
else StatusMessage = Loc.T("vm.settingsModal.workerOffline");
await Prime.LoadAsync();
}
@@ -74,12 +87,16 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
Worktrees.WorktreeStrategy ?? "sibling",
string.IsNullOrWhiteSpace(Worktrees.CentralWorktreeRoot) ? null : Worktrees.CentralWorktreeRoot,
Worktrees.WorktreeAutoCleanupEnabled,
Worktrees.WorktreeAutoCleanupDays);
Worktrees.WorktreeAutoCleanupDays,
System.Text.Json.JsonSerializer.Serialize(
General.ReportExcludedPaths
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
General.StandupWeekday);
await _worker.UpdateAppSettingsAsync(dto);
await Prime.SaveAsync();
CloseAction?.Invoke();
}
catch (Exception ex) { StatusMessage = $"Save failed: {ex.Message}"; }
catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
finally { IsBusy = false; }
}

View File

@@ -0,0 +1,88 @@
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 WeeklyReportModalViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
public WeeklyReportModalViewModel(IWorkerClient worker) => _worker = worker;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasReport))]
[NotifyPropertyChangedFor(nameof(EmptyStateVisible))]
private string? _reportMarkdown;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EmptyStateVisible))]
[NotifyCanExecuteChangedFor(nameof(GenerateCommand))]
private bool _isBusy;
[ObservableProperty] private DateTime? _startDate;
[ObservableProperty] private DateTime? _endDate;
[ObservableProperty] private string _statusMessage = "";
public bool HasReport => !string.IsNullOrWhiteSpace(ReportMarkdown);
public bool EmptyStateVisible => !HasReport && !IsBusy;
public Action? CloseAction { get; set; }
[RelayCommand] private void Close() => CloseAction?.Invoke();
public static (DateOnly Start, DateOnly End) DefaultRange(DayOfWeek standup, DateOnly today)
{
int diff = ((int)today.DayOfWeek - (int)standup + 7) % 7;
if (diff == 0) diff = 7;
return (today.AddDays(-diff), today);
}
public async Task InitializeAsync()
{
var standup = DayOfWeek.Wednesday;
var settings = await _worker.GetAppSettingsAsync();
if (settings is not null && settings.StandupWeekday is >= 0 and <= 6)
standup = (DayOfWeek)settings.StandupWeekday;
var (start, end) = DefaultRange(standup, DateOnly.FromDateTime(DateTime.Today));
StartDate = start.ToDateTime(TimeOnly.MinValue);
EndDate = end.ToDateTime(TimeOnly.MinValue);
await LoadStoredAsync();
}
partial void OnStartDateChanged(DateTime? value) => _ = LoadStoredAsync();
partial void OnEndDateChanged(DateTime? value) => _ = LoadStoredAsync();
private bool RangeValid => StartDate is not null && EndDate is not null && StartDate <= EndDate;
private async Task LoadStoredAsync()
{
if (!RangeValid) return;
StatusMessage = "";
try
{
ReportMarkdown = await _worker.GetWeekReportAsync(
DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value));
}
catch (Exception ex) { StatusMessage = ex.Message; }
}
private bool CanGenerate() => !IsBusy;
[RelayCommand(CanExecute = nameof(CanGenerate))]
private async Task Generate()
{
if (!RangeValid) { StatusMessage = Loc.T("vm.weeklyReport.invalidRange"); return; }
IsBusy = true;
StatusMessage = Loc.T("vm.weeklyReport.generating");
try
{
ReportMarkdown = await _worker.GenerateWeekReportAsync(
DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value));
StatusMessage = "";
}
catch (Exception ex) { StatusMessage = Loc.T("vm.weeklyReport.error", ex.Message); }
finally { IsBusy = false; }
}
}

View File

@@ -4,6 +4,7 @@ using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -86,7 +87,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
{
ListIdFilter = listId;
IsGlobal = listId is null;
Title = listId is null ? "Worktrees" : $"Worktrees — {listName ?? "list"}";
Title = listId is null
? Loc.T("vm.worktreesOverview.titleAll")
: Loc.T("vm.worktreesOverview.titleList", listName ?? Loc.T("vm.worktreesOverview.listFallback"));
}
public async Task LoadAsync(CancellationToken ct = default)
@@ -138,7 +141,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
try
{
var result = await _worker.CleanupFinishedWorktreesAsync(ListIdFilter);
StatusMessage = result is null ? "Cleanup failed." : $"Removed {result.Removed} worktree(s).";
StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed);
await LoadAsync();
}
finally { IsBusy = false; }
@@ -190,7 +193,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
if (row is null || row.State != WorktreeState.Active) return;
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded);
if (ok) row.State = WorktreeState.Discarded;
else StatusMessage = err ?? "Failed to discard worktree.";
else StatusMessage = err ?? Loc.T("vm.worktreesOverview.discardFailed");
}
[RelayCommand]
@@ -199,20 +202,20 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
if (row is null || row.State != WorktreeState.Active) return;
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept);
if (ok) row.State = WorktreeState.Kept;
else StatusMessage = err ?? "Failed to keep worktree.";
else StatusMessage = err ?? Loc.T("vm.worktreesOverview.keepFailed");
}
[RelayCommand]
private async Task ForceRemove(WorktreeOverviewRowViewModel? row)
{
if (row is null) return;
if (row.IsRunning) { StatusMessage = "Cannot force-remove a running task."; 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;
var result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
if (result is null || !result.Removed)
{
StatusMessage = result?.Reason ?? "Force remove failed.";
StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
return;
}
if (IsGlobal)

View File

@@ -1,6 +1,7 @@
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Planning;
@@ -14,6 +15,8 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject
public string SubtaskTitle { get; }
public string TargetBranch { get; }
public IReadOnlyList<string> ConflictedFiles { get; }
public string SubtaskLabel => Loc.T("vm.conflictResolution.subtaskPrefix", SubtaskTitle);
public string TargetLabel => Loc.T("vm.conflictResolution.targetPrefix", TargetBranch);
[ObservableProperty] private string? _vsCodeError;
[ObservableProperty] private string? _actionError;
@@ -53,7 +56,7 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject
}
catch (Exception ex)
{
VsCodeError = $"Could not launch VS Code: {ex.Message}. Paths are listed above — copy them manually.";
VsCodeError = Loc.T("vm.conflictResolution.vsCodeError", ex.Message);
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Planning;
@@ -59,7 +60,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
if (result is null)
{
DisplayedDiff = "";
CombinedWarning = "Could not build combined preview (hub error).";
CombinedWarning = Loc.T("vm.planningDiff.hubError");
}
else if (result.Success)
{
@@ -69,7 +70,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
else
{
var files = result.ConflictedFiles?.Count ?? 0;
CombinedWarning = $"Cannot build combined preview: subtask {result.FirstConflictSubtaskId} conflicts with an earlier subtask ({files} files).";
CombinedWarning = Loc.T("vm.planningDiff.conflict", result.FirstConflictSubtaskId ?? "", files);
DisplayedDiff = "";
}
}

View File

@@ -1,5 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Controls.ThemedDatePicker"
x:Name="Root">
@@ -125,10 +126,10 @@
MinWidth="300">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Classes="quick" Content="Today" Click="OnTodayClick"/>
<Button Classes="quick" Content="Tomorrow" Click="OnTomorrowClick"/>
<Button Classes="quick" Content="Next Mon" Click="OnNextMondayClick"/>
<Button Classes="quick" Content="Clear" Click="OnClearClick"/>
<Button Classes="quick" Content="{loc:Tr controls.datePicker.today}" Click="OnTodayClick"/>
<Button Classes="quick" Content="{loc:Tr controls.datePicker.tomorrow}" Click="OnTomorrowClick"/>
<Button Classes="quick" Content="{loc:Tr controls.datePicker.nextMon}" Click="OnNextMondayClick"/>
<Button Classes="quick" Content="{loc:Tr controls.datePicker.clear}" Click="OnClearClick"/>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,2,0,0">
@@ -146,14 +147,14 @@
<Grid x:Name="TimeRow"
ColumnDefinitions="Auto,*,Auto"
Margin="0,4,0,0">
<TextBlock Grid.Column="0" Text="Time"
<TextBlock Grid.Column="0" Text="{loc:Tr controls.datePicker.time}"
VerticalAlignment="Center"
Foreground="{DynamicResource TextDimBrush}"
Margin="0,0,8,0"/>
<TextBox Grid.Column="1" x:Name="TimeInput"
PlaceholderText="HH:mm" MaxLength="5"
Text="{Binding #Root.TimeText, Mode=TwoWay}"/>
<Button Grid.Column="2" Content="Done"
<Button Grid.Column="2" Content="{loc:Tr controls.datePicker.done}"
Click="OnDoneClick"
Margin="8,0,0,0"/>
</Grid>

View File

@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.AgentStripView"
x:DataType="vm:DetailsIslandViewModel">
<Border Classes="agent-strip"
@@ -34,7 +35,7 @@
Classes="icon-btn"
Command="{Binding StopCommand}"
IsVisible="{Binding IsRunning}"
ToolTip.Tip="Stop agent"
ToolTip.Tip="{loc:Tr agent.stopTip}"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.X}" Width="12" Height="12"
Foreground="{DynamicResource BloodBrush}"/>
@@ -42,19 +43,19 @@
<!-- Send to queue — only when idle -->
<Button Grid.Column="3"
Classes="btn accent"
Content="Send to queue"
Content="{loc:Tr agent.sendToQueue}"
Command="{Binding EnqueueCommand}"
IsVisible="{Binding IsIdle}"
ToolTip.Tip="Queue this task for the worker to pick up"
ToolTip.Tip="{loc:Tr agent.sendToQueueTip}"
VerticalAlignment="Center"
Padding="10,4"/>
<!-- Remove from queue — only when queued -->
<Button Grid.Column="3"
Classes="btn"
Content="Remove from queue"
Content="{loc:Tr agent.removeFromQueue}"
Command="{Binding DequeueCommand}"
IsVisible="{Binding IsQueued}"
ToolTip.Tip="Take this task back out of the queue"
ToolTip.Tip="{loc:Tr agent.removeFromQueueTip}"
VerticalAlignment="Center"
Padding="10,4"/>
</Grid>
@@ -64,7 +65,7 @@
IsVisible="{Binding WorktreePath, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Grid.Column="0"
Classes="eyebrow"
Text="WORKTREE"
Text="{loc:Tr agent.worktreeLabel}"
Foreground="{DynamicResource TextFaintBrush}"
LetterSpacing="1.2"
VerticalAlignment="Center"
@@ -76,7 +77,7 @@
VerticalAlignment="Center"/>
<Button Grid.Column="2"
Classes="icon-btn"
ToolTip.Tip="Copy path"
ToolTip.Tip="{loc:Tr agent.copyPathTip}"
Click="OnCopyWorktreePathClick"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
@@ -105,7 +106,7 @@
<Grid ColumnDefinitions="Auto,Auto,Auto,*">
<TextBlock Grid.Column="0"
Classes="eyebrow"
Text="DIFF"
Text="{loc:Tr agent.diffLabel}"
Foreground="{DynamicResource TextFaintBrush}"
LetterSpacing="1.2"
VerticalAlignment="Center"
@@ -136,27 +137,27 @@
<!-- Action buttons -->
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
<Button Classes="btn" Content="Open diff" Command="{Binding OpenDiffCommand}"/>
<Button Classes="btn" Content="{loc:Tr agent.openDiff}" Command="{Binding OpenDiffCommand}"/>
<Button Classes="btn" Command="{Binding OpenWorktreeCommand}"
ToolTip.Tip="Open worktree in file explorer">
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}">
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.ArrowOut}"
Width="11" Height="11"
Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Text="Worktree" VerticalAlignment="Center"/>
<TextBlock Text="{loc:Tr agent.worktreeBtn}" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Classes="btn accent"
Content="Continue"
Content="{loc:Tr agent.continue}"
Command="{Binding ContinueCommand}"
IsVisible="{Binding ShowContinue}"
ToolTip.Tip="Resume the last session and keep going"
ToolTip.Tip="{loc:Tr agent.continueTip}"
Padding="10,4"/>
<Button Classes="btn"
Content="Reset &amp; retry"
Content="{loc:Tr agent.resetAndRetry}"
Command="{Binding ResetAndRetryCommand}"
IsVisible="{Binding ShowResetAndRetry}"
ToolTip.Tip="Discard the worktree and re-queue the task to run from scratch"
ToolTip.Tip="{loc:Tr agent.resetAndRetryTip}"
Padding="10,4"/>
</StackPanel>

View File

@@ -3,6 +3,7 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
x:DataType="vm:DetailsIslandViewModel">
<DockPanel>
@@ -15,7 +16,7 @@
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Grid.Column="0" Classes="icon-btn"
Command="{Binding DeleteTaskCommand}"
ToolTip.Tip="Delete task"
ToolTip.Tip="{loc:Tr details.deleteTaskTip}"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.Trash}" Width="14" Height="14"
Foreground="{DynamicResource BloodBrush}"/>
@@ -27,7 +28,7 @@
VerticalAlignment="Center"/>
<Button Grid.Column="2" Classes="icon-btn"
Command="{Binding CloseDetailsCommand}"
ToolTip.Tip="Close"
ToolTip.Tip="{loc:Tr details.closeTip}"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.X}" Width="14" Height="14"/>
</Button>
@@ -52,7 +53,7 @@
Text="{Binding TaskIdBadge}"
Margin="0,0,0,4"
Cursor="Hand"
ToolTip.Tip="Copy task ID"
ToolTip.Tip="{loc:Tr details.copyTaskIdTip}"
Tapped="OnTaskIdTapped"/>
<TextBox Text="{Binding EditableTitle, Mode=TwoWay}"
FontSize="{StaticResource FontSizeTaskTitle}" FontWeight="Medium"
@@ -67,14 +68,14 @@
Classes="icon-btn star-btn"
Classes.on="{Binding Task.IsStarred}"
Command="{Binding ToggleStarCommand}"
ToolTip.Tip="Star"
ToolTip.Tip="{loc:Tr details.starTip}"
VerticalAlignment="Top"
Margin="6,0,0,0">
<PathIcon Data="{StaticResource Icon.Star}" Width="14" Height="14"/>
</Button>
<Button Grid.Column="3" Classes="icon-btn"
ToolTip.Tip="Agent settings"
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
IsEnabled="{Binding IsAgentSectionEnabled}"
VerticalAlignment="Top"
Margin="6,0,0,0">
@@ -82,27 +83,27 @@
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight" ShowMode="Standard">
<StackPanel Width="340" Spacing="10" Margin="4">
<TextBlock Text="Agent settings (overrides)" FontWeight="SemiBold"/>
<TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/>
<StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="Model"/>
<TextBlock Classes="field-label" Text="{loc:Tr details.modelLabel}"/>
<ComboBox ItemsSource="{Binding TaskModelOptions}"
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
<TextBlock Classes="meta"
Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}"
Text="{Binding EffectiveModelLabel}"
Opacity="0.6"/>
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="System prompt (appended)"/>
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"
PlaceholderText="{Binding EffectiveSystemPromptHint}"/>
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="Agent file"/>
<TextBlock Classes="field-label" Text="{loc:Tr details.agentFileLabel}"/>
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch">
@@ -113,7 +114,7 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Classes="meta"
Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}"
Text="{Binding EffectiveAgentLabel}"
Opacity="0.6"/>
</StackPanel>
</StackPanel>
@@ -126,25 +127,27 @@
<!-- ── Agent status strip (sticky, above metadata footer) ── -->
<islands:AgentStripView DockPanel.Dock="Bottom"/>
<!-- ── Scrollable body: steps + terminal ── -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
<!-- ── Body: task details (normal) or notes editor (notes mode) ── -->
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto"
IsVisible="{Binding !IsNotesMode}">
<StackPanel Spacing="0">
<!-- Planning merge section — visible only for planning parent tasks -->
<Border Classes="section-divider"
IsVisible="{Binding Task.IsPlanningParent}">
<StackPanel Spacing="8">
<TextBlock Classes="section-label" Text="MERGE" Margin="0,0,0,2"/>
<TextBlock Classes="section-label" Text="{loc:Tr details.mergeLabel}" Margin="0,0,0,2"/>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Merge target"/>
<TextBlock Classes="field-label" Text="{loc:Tr details.mergeTargetLabel}"/>
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="Review combined diff"
<Button Classes="btn" Content="{loc:Tr details.reviewCombinedDiff}"
Command="{Binding ReviewCombinedDiffCommand}"/>
<Button Classes="btn" Content="Merge all subtasks"
<Button Classes="btn" Content="{loc:Tr details.mergeAllSubtasks}"
IsEnabled="{Binding CanMergeAll}"
Command="{Binding MergeAllCommand}"
ToolTip.Tip="{Binding MergeAllDisabledReason}"/>
@@ -159,9 +162,9 @@
<!-- Steps section -->
<Border Classes="section-divider">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="STEPS" Margin="0,0,0,2"/>
<TextBlock Classes="section-label" Text="{loc:Tr details.stepsLabel}" Margin="0,0,0,2"/>
<TextBox Text="{Binding NewSubtaskTitle, Mode=TwoWay}"
PlaceholderText="Add a step..."
PlaceholderText="{loc:Tr details.addStepPlaceholder}"
Padding="8"
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
@@ -238,14 +241,14 @@
<TextBlock Classes="meta"
Text="▸"
IsVisible="{Binding !IsDescriptionExpanded}"/>
<TextBlock Classes="section-label" Text="DETAILS"/>
<TextBlock Classes="section-label" Text="{loc:Tr details.detailsLabel}"/>
</StackPanel>
</Button>
<Button Grid.Column="2"
Classes="icon-btn"
Padding="6,2"
Margin="0,0,4,0"
ToolTip.Tip="Copy description to clipboard"
ToolTip.Tip="{loc:Tr details.copyDescriptionTip}"
IsVisible="{Binding IsDescriptionExpanded}"
Click="OnCopyDescriptionClick">
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
@@ -254,17 +257,17 @@
Classes="btn"
Command="{Binding ToggleEditDescriptionCommand}"
Padding="8,3"
ToolTip.Tip="Toggle edit/preview"
ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}"
IsVisible="{Binding IsDescriptionEditorVisible}">
<TextBlock Text="Preview"/>
<TextBlock Text="{loc:Tr details.previewBtn}"/>
</Button>
<Button Grid.Column="3"
Classes="btn"
Command="{Binding ToggleEditDescriptionCommand}"
Padding="8,3"
ToolTip.Tip="Toggle edit/preview"
ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}"
IsVisible="{Binding IsDescriptionPreviewVisible}">
<TextBlock Text="Edit"/>
<TextBlock Text="{loc:Tr details.editBtn}"/>
</Button>
</Grid>
@@ -273,7 +276,7 @@
TextWrapping="Wrap"
MinHeight="80"
MaxHeight="320"
PlaceholderText="Add task details (markdown supported)..."
PlaceholderText="{loc:Tr details.descriptionPlaceholder}"
Padding="8"
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeBody}"
@@ -293,6 +296,10 @@
</StackPanel>
</ScrollViewer>
<Panel IsVisible="{Binding IsNotesMode}">
<islands:NotesEditorView DataContext="{Binding Notes}"/>
</Panel>
</Grid>
</DockPanel>
</UserControl>

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<DockPanel LastChildFill="True">
@@ -9,7 +10,7 @@
<!-- ── Header ── -->
<Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Spacing="4">
<TextBlock Classes="heading" Text="Lists"/>
<TextBlock Classes="heading" Text="{loc:Tr lists.heading}"/>
<!-- Search row -->
<Border Classes="search-wrap" Margin="0,8,0,12">
@@ -19,10 +20,10 @@
Foreground="{DynamicResource TextFaintBrush}"
Margin="2,0,0,0"/>
<TextBox Grid.Column="1" x:Name="SearchBox" Classes="search-inner"
PlaceholderText="Search tasks…"
PlaceholderText="{loc:Tr lists.searchPlaceholder}"
Text="{Binding SearchText, Mode=TwoWay}"/>
<Border Grid.Column="2" Classes="kbd" Margin="0,0,2,0">
<TextBlock Text="Ctrl K"/>
<TextBlock Text="{loc:Tr lists.searchKbd}"/>
</Border>
</Grid>
</Border>
@@ -46,18 +47,12 @@
<!-- Name + machine -->
<StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center">
<TextBlock Classes="title" Text="{Binding UserName}"/>
<TextBlock Classes="meta">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} / local">
<Binding Path="MachineName"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Classes="meta" Text="{Binding MachineNameLocal}"/>
</StackPanel>
<!-- More button -->
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
Command="{Binding OpenSettingsCommand}"
ToolTip.Tip="Settings">
ToolTip.Tip="{loc:Tr lists.settingsTip}">
<PathIcon Data="{StaticResource Icon.MoreHorizontal}"
Width="14" Height="14"
Foreground="{DynamicResource TextMuteBrush}"/>
@@ -70,7 +65,7 @@
<StackPanel Margin="6,0,6,4">
<!-- SMART LISTS section -->
<TextBlock Classes="section-label" Text="SMART LISTS" Margin="10,10,10,4"/>
<TextBlock Classes="section-label" Text="{loc:Tr lists.smartListsLabel}" Margin="10,10,10,4"/>
<ItemsControl ItemsSource="{Binding SmartLists}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
@@ -109,7 +104,7 @@
</ItemsControl>
<!-- MY LISTS section -->
<TextBlock Classes="section-label" Text="MY LISTS" Margin="10,10,10,4"/>
<TextBlock Classes="section-label" Text="{loc:Tr lists.myListsLabel}" Margin="10,10,10,4"/>
<ItemsControl ItemsSource="{Binding UserLists}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
@@ -127,18 +122,18 @@
DragDrop.Drop="OnListDrop">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Settings..."
<MenuItem Header="{loc:Tr lists.contextSettings}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenListSettingsCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="Worktrees"
<MenuItem Header="{loc:Tr lists.contextWorktrees}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenWorktreesOverviewCommand}"
CommandParameter="{Binding}"/>
<Separator IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<MenuItem Header="Open in Explorer"
<MenuItem Header="{loc:Tr lists.contextOpenExplorer}"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInExplorerCommand}"
CommandParameter="{Binding}"/>
<MenuItem Header="Open in Terminal"
<MenuItem Header="{loc:Tr lists.contextOpenTerminal}"
IsVisible="{Binding WorkingDir, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding $parent[UserControl].((vm:ListsIslandViewModel)DataContext).OpenInTerminalCommand}"
CommandParameter="{Binding}"/>
@@ -191,14 +186,14 @@
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/>
<TextBlock Classes="body"
Text="New list"
Text="{loc:Tr lists.newList}"
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Grid.Column="1" Classes="icon-btn" Margin="6,0,0,0"
Command="{Binding OpenRepoImportCommand}"
ToolTip.Tip="Add repos as lists">
ToolTip.Tip="{loc:Tr lists.addReposTip}">
<PathIcon Data="{StaticResource Icon.Folder}"
Width="14" Height="14"
Foreground="{DynamicResource TextMuteBrush}"/>

View File

@@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.NotesEditorView"
x:DataType="vm:NotesEditorViewModel">
<DockPanel Margin="16">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="&#x2039;" Command="{Binding PrevDayCommand}"/>
<ctl:ThemedDatePicker SelectedDate="{Binding CurrentDate, Mode=TwoWay}"/>
<Button Classes="btn" Content="&#x203A;" Command="{Binding NextDayCommand}"/>
<Button Classes="btn" Content="{loc:Tr notes.today}" Command="{Binding TodayCommand}"/>
<TextBlock Classes="meta" VerticalAlignment="Center" Text="{Binding CurrentDayLabel}"/>
</StackPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,12,0,8">
<Button DockPanel.Dock="Right" Classes="btn" Content="{loc:Tr notes.add}" Margin="8,0,0,0"
Command="{Binding AddBulletCommand}"/>
<TextBox PlaceholderText="{loc:Tr notes.newNotePlaceholder}" Text="{Binding NewBulletText}">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddBulletCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Bullets}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:NoteBulletViewModel">
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,2" ColumnSpacing="6">
<TextBox Grid.Column="0" Text="{Binding Text}"/>
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr notes.save}" Command="{Binding SaveCommand}"/>
<Button Grid.Column="2" Classes="btn" Content="{loc:Tr notes.delete}" Command="{Binding DeleteCommand}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</UserControl>

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.Islands;
public partial class NotesEditorView : UserControl
{
public NotesEditorView() => InitializeComponent();
}

View File

@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.SessionTerminalView"
x:DataType="vm:DetailsIslandViewModel">
<Border Classes="terminal" Margin="18,8,18,0">
@@ -24,7 +25,7 @@
Margin="0,0,8,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center"/>
<TextBlock Text="LIVE" VerticalAlignment="Center"/>
<TextBlock Text="{loc:Tr session.chipLive}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- DONE chip -->
@@ -33,7 +34,7 @@
Margin="0,0,8,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource MossBrush}"/>
<TextBlock Text="DONE" VerticalAlignment="Center"
<TextBlock Text="{loc:Tr session.chipDone}" VerticalAlignment="Center"
Foreground="{DynamicResource MossBrush}"/>
</StackPanel>
</Border>
@@ -43,7 +44,7 @@
Margin="0,0,8,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource BloodBrush}"/>
<TextBlock Text="FAILED" VerticalAlignment="Center"
<TextBlock Text="{loc:Tr session.chipFailed}" VerticalAlignment="Center"
Foreground="{DynamicResource BloodBrush}"/>
</StackPanel>
</Border>

View File

@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
x:DataType="vm:TaskRowViewModel">
<Grid>
@@ -32,38 +33,38 @@
Classes.done="{Binding Done}">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Send to queue"
<MenuItem Header="{loc:Tr tasks.ctxSendToQueue}"
IsVisible="{Binding CanSendToQueue}"
Click="OnSendToQueueClick"/>
<MenuItem Header="Remove from queue"
<MenuItem Header="{loc:Tr tasks.ctxRemoveFromQueue}"
IsVisible="{Binding CanRemoveFromQueue}"
Click="OnRemoveFromQueueClick"/>
<MenuItem Header="Cancel execution"
<MenuItem Header="{loc:Tr tasks.ctxCancelExecution}"
IsVisible="{Binding IsRunning}"
Click="OnCancelExecutionClick"/>
<Separator/>
<MenuItem Header="Mark as">
<MenuItem Header="Done" Tag="Done" Click="OnSetStatusClick"/>
<MenuItem Header="Cancelled" Tag="Cancelled" Click="OnSetStatusClick"/>
<MenuItem Header="{loc:Tr tasks.ctxMarkAs}">
<MenuItem Header="{loc:Tr tasks.ctxMarkDone}" Tag="Done" Click="OnSetStatusClick"/>
<MenuItem Header="{loc:Tr tasks.ctxMarkCancelled}" Tag="Cancelled" Click="OnSetStatusClick"/>
</MenuItem>
<Separator/>
<MenuItem Header="Run interactively"
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
Click="OnRunInteractivelyClick"/>
<MenuItem Header="Open planning Session"
<MenuItem Header="{loc:Tr tasks.ctxOpenPlanningSession}"
Click="OnOpenPlanningSessionClick"
IsVisible="{Binding CanOpenPlanningSession}"/>
<MenuItem Header="Resume planning Session"
<MenuItem Header="{loc:Tr tasks.ctxResumePlanningSession}"
Click="OnResumePlanningSessionClick"
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
<MenuItem Header="Discard planning session"
<MenuItem Header="{loc:Tr tasks.ctxDiscardPlanningSession}"
Click="OnDiscardPlanningSessionClick"
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
<MenuItem Header="Queue subtasks sequentially"
<MenuItem Header="{loc:Tr tasks.ctxQueueSubtasks}"
Click="OnQueuePlanningSubtasksClick"
IsVisible="{Binding CanQueuePlan}"/>
<Separator/>
<MenuItem Header="Schedule for..." Click="OnScheduleForClick"/>
<MenuItem Header="Clear schedule"
<MenuItem Header="{loc:Tr tasks.ctxScheduleFor}" Click="OnScheduleForClick"/>
<MenuItem Header="{loc:Tr tasks.ctxClearSchedule}"
IsVisible="{Binding HasSchedule}"
Click="OnClearScheduleClick"/>
</ContextMenu>
@@ -111,10 +112,10 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4"
VerticalAlignment="Center" Margin="4,0,0,0">
<Border Classes="badge draft" IsVisible="{Binding IsDraft}">
<TextBlock Text="DRAFT"/>
<TextBlock Text="{loc:Tr tasks.badgeDraft}"/>
</Border>
<Border Classes="badge planned" IsVisible="{Binding IsPlanned}">
<TextBlock Text="PLANNED"/>
<TextBlock Text="{loc:Tr tasks.badgePlanned}"/>
</Border>
<Border Classes="badge"
Classes.planning="{Binding IsPlanActive}"
@@ -141,24 +142,24 @@
<!-- Review actions (visible when WaitingForReview) -->
<StackPanel Orientation="Horizontal" Spacing="4"
IsVisible="{Binding IsWaitingForReview}">
<Button Classes="btn" Content="Approve" MinWidth="0" Padding="8,2"
ToolTip.Tip="Approve — mark Done"
<Button Classes="btn" Content="{loc:Tr tasks.approve}" MinWidth="0" Padding="8,2"
ToolTip.Tip="{loc:Tr tasks.approveTip}"
Click="OnApproveReviewClick"/>
<Button Classes="btn" Content="Reject" MinWidth="0" Padding="8,2"
ToolTip.Tip="Reject with feedback and re-run"
<Button Classes="btn" Content="{loc:Tr tasks.reject}" MinWidth="0" Padding="8,2"
ToolTip.Tip="{loc:Tr tasks.rejectTip}"
Click="OnRejectReviewClick"/>
<Button Classes="btn" Content="Park" MinWidth="0" Padding="8,2"
ToolTip.Tip="Send back to Idle for manual editing"
<Button Classes="btn" Content="{loc:Tr tasks.park}" MinWidth="0" Padding="8,2"
ToolTip.Tip="{loc:Tr tasks.parkTip}"
Click="OnParkReviewClick"/>
<Button Classes="btn" Content="Cancel" MinWidth="0" Padding="8,2"
ToolTip.Tip="Cancel this task"
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" MinWidth="0" Padding="8,2"
ToolTip.Tip="{loc:Tr tasks.cancelTip}"
Click="OnCancelReviewClick"/>
</StackPanel>
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
<Button Classes="icon-btn dequeue-btn"
IsVisible="{Binding CanRemoveFromQueue}"
ToolTip.Tip="Remove from queue"
ToolTip.Tip="{loc:Tr tasks.removeFromQueueTip}"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RemoveFromQueueCommand}"
CommandParameter="{Binding}">
<PathIcon Width="10" Height="10" Data="{StaticResource Icon.X}"/>
@@ -226,10 +227,10 @@
BorderThickness="1" CornerRadius="10"
Padding="16" Width="300">
<StackPanel Spacing="12">
<TextBlock Classes="title" Text="Schedule task"/>
<TextBlock Classes="title" Text="{loc:Tr tasks.scheduleTitle}"/>
<StackPanel Spacing="6">
<TextBlock Classes="eyebrow" Text="WHEN"
<TextBlock Classes="eyebrow" Text="{loc:Tr tasks.scheduleWhen}"
Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
<ctl:ThemedDatePicker x:Name="ScheduleDate" ShowTime="True"
HorizontalAlignment="Stretch"/>
@@ -237,8 +238,8 @@
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,4,0,0">
<Button Classes="btn" Content="Cancel" Click="OnScheduleCancelClick" MinWidth="76"/>
<Button Content="Schedule" Classes="accent" Click="OnScheduleSetClick" MinWidth="76"/>
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" Click="OnScheduleCancelClick" MinWidth="76"/>
<Button Content="{loc:Tr tasks.scheduleConfirm}" Classes="accent" Click="OnScheduleSetClick" MinWidth="76"/>
</StackPanel>
</StackPanel>
</Border>
@@ -258,18 +259,18 @@
BorderThickness="1" CornerRadius="10"
Padding="16" Width="320">
<StackPanel Spacing="12">
<TextBlock Classes="title" Text="Reject &amp; re-run"/>
<TextBlock Classes="title" Text="{loc:Tr tasks.rejectRerunTitle}"/>
<StackPanel Spacing="6">
<TextBlock Classes="eyebrow" Text="FEEDBACK FOR THE AGENT"
<TextBlock Classes="eyebrow" Text="{loc:Tr tasks.feedbackLabel}"
Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
<TextBox x:Name="RejectFeedback"
AcceptsReturn="True" TextWrapping="Wrap"
MinHeight="80" PlaceholderText="What should the agent fix?"/>
MinHeight="80" PlaceholderText="{loc:Tr tasks.feedbackPlaceholder}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="0,4,0,0">
<Button Classes="btn" Content="Cancel" Click="OnRejectCancelClick" MinWidth="76"/>
<Button Content="Re-run" Classes="accent" Click="OnRejectConfirmClick" MinWidth="76"/>
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" Click="OnRejectCancelClick" MinWidth="76"/>
<Button Content="{loc:Tr tasks.rerun}" Classes="accent" Click="OnRejectConfirmClick" MinWidth="76"/>
</StackPanel>
</StackPanel>
</Border>

View File

@@ -3,6 +3,7 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:converters="using:Avalonia.Data.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.TasksIslandView"
x:DataType="vm:TasksIslandViewModel">
<DockPanel LastChildFill="True">
@@ -27,15 +28,15 @@
IsVisible="{Binding HasStatusPill}">
<TextBlock Text="{Binding StatusPill}"/>
</Border>
<Button Classes="icon-btn" Command="{Binding SortCommand}" ToolTip.Tip="Sort">
<Button Classes="icon-btn" Command="{Binding SortCommand}" ToolTip.Tip="{loc:Tr tasks.sortTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Sort}"/>
</Button>
<Button Classes="icon-btn" Classes.active="{Binding IsShowingCompleted}"
Command="{Binding ToggleShowCompletedCommand}"
ToolTip.Tip="Show completed">
ToolTip.Tip="{loc:Tr tasks.showCompletedTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/>
</Button>
<Button Classes="icon-btn" Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="List settings">
<Button Classes="icon-btn" Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}">
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/>
</Button>
</StackPanel>
@@ -50,7 +51,7 @@
Foreground="{DynamicResource TextFaintBrush}"/>
</Border>
<TextBox Grid.Column="1" x:Name="AddTaskBox" Classes="add-task-input"
PlaceholderText="Add a task…"
PlaceholderText="{loc:Tr tasks.addPlaceholder}"
Text="{Binding NewTaskTitle, Mode=TwoWay}"
VerticalAlignment="Center"
Margin="12,0,0,0">
@@ -60,11 +61,19 @@
</TextBox>
<Border Grid.Column="2" Classes="kbd kbd-enter" VerticalAlignment="Center"
IsVisible="{Binding #AddTaskBox.IsFocused}">
<TextBlock Text="ENTER"/>
<TextBlock Text="{loc:Tr tasks.enterKey}"/>
</Border>
</Grid>
</Border>
<!-- Notes pinned row (My Day only) -->
<Button DockPanel.Dock="Top"
Classes="btn" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Margin="16,0,16,8"
IsVisible="{Binding ShowNotesRow}"
Command="{Binding OpenNotesCommand}"
Content="{loc:Tr tasks.notesPinnedRow}"/>
<!-- Task list -->
<ScrollViewer>
<StackPanel Margin="10,4">
@@ -72,7 +81,7 @@
<!-- OVERDUE -->
<StackPanel IsVisible="{Binding HasOverdue}">
<TextBlock Classes="eyebrow section-label overdue"
Text="OVERDUE" Margin="14,14,14,6"/>
Text="{loc:Tr tasks.overdue}" Margin="14,14,14,6"/>
<ItemsControl ItemsSource="{Binding OverdueItems}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskRowViewModel">
@@ -96,7 +105,7 @@
<!-- TASKS -->
<StackPanel IsVisible="{Binding HasOpen}">
<TextBlock Classes="eyebrow section-label"
Text="TASKS" Margin="14,14,14,6"
Text="{loc:Tr tasks.tasks}" Margin="14,14,14,6"
IsVisible="{Binding ShowOpenLabel}"/>
<ItemsControl ItemsSource="{Binding OpenItems}">
<ItemsControl.ItemTemplate>
@@ -131,7 +140,7 @@
Text="{Binding CompletedHeader}" VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn"
Command="{Binding ClearCompletedCommand}"
ToolTip.Tip="Clear all completed"
ToolTip.Tip="{loc:Tr tasks.clearCompletedTip}"
VerticalAlignment="Center">
<PathIcon Data="{StaticResource Icon.Trash}" Width="13" Height="13"
Foreground="{DynamicResource BloodBrush}"/>

View File

@@ -3,6 +3,7 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.MainWindow"
x:DataType="vm:IslandsShellViewModel"
Title="ClaudeDo"
@@ -56,17 +57,18 @@
<Menu Margin="12,0,0,0"
Background="Transparent"
VerticalAlignment="Center">
<MenuItem Header="Help"
<MenuItem Header="{loc:Tr shell.menu.help}"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource TextDimBrush}">
<MenuItem Header="Check for updates"
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
Command="{Binding CheckForUpdatesCommand}"/>
<MenuItem Header="Restart worker"
<MenuItem Header="{loc:Tr shell.menu.restartWorker}"
Command="{Binding RestartWorkerCommand}"/>
<MenuItem Header="Worktrees"
<MenuItem Header="{loc:Tr shell.menu.worktrees}"
Command="{Binding OpenWorktreesOverviewGlobalCommand}"/>
<MenuItem Header="About…" Command="{Binding OpenAboutCommand}"/>
<MenuItem Header="Add repos as lists…" Command="{Binding OpenRepoImportCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
<MenuItem Header="{loc:Tr shell.menu.addRepos}" Command="{Binding OpenRepoImportCommand}"/>
</MenuItem>
</Menu>
</StackPanel>
@@ -103,7 +105,7 @@
<TextBlock Grid.Column="0"
Classes="body"
VerticalAlignment="Center">
<Run Text="Update available: v"/>
<Run Text="{loc:Tr shell.update.available}"/>
<Run Text="{Binding UpdateCheck.CurrentVersion}"/>
<Run Text=" → v"/>
<Run Text="{Binding UpdateBannerLatestVersion}"/>
@@ -111,11 +113,11 @@
<Button Grid.Column="1"
Classes="btn"
Margin="0,0,8,0"
Content="Update now"
Content="{loc:Tr shell.update.updateNow}"
Command="{Binding UpdateNowCommand}"/>
<Button Grid.Column="2"
Classes="btn"
Content="Dismiss"
Content="{loc:Tr shell.update.dismiss}"
Command="{Binding DismissBannerCommand}"/>
</Grid>
</Border>

View File

@@ -52,6 +52,12 @@ public partial class MainWindow : Window
aboutVm.CloseAction = () => { dlg.Close(); tcs.TrySetResult(true); };
await dlg.ShowDialog(this);
};
vm.ShowWeeklyReportModal = async (modal) =>
{
var dlg = new WeeklyReportModalView { DataContext = modal };
modal.CloseAction = () => dlg.Close();
await dlg.ShowDialog(this);
};
vm.ShowWorktreesOverviewModal = async (modal) =>
{
var dlg = new WorktreesOverviewModalView { DataContext = modal };

View File

@@ -2,9 +2,10 @@
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"
x:Class="ClaudeDo.Ui.Views.Modals.AboutModalView"
x:DataType="vm:AboutModalViewModel"
Title="About ClaudeDo"
Title="{loc:Tr modals.about.title}"
Width="620" Height="280"
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
@@ -14,21 +15,21 @@
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="ABOUT" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell Title="{loc:Tr modals.about.title}" CloseCommand="{Binding CloseCommand}">
<!-- Body -->
<ScrollViewer Padding="20,16" HorizontalScrollBarVisibility="Disabled">
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="10" ColumnSpacing="8">
<TextBlock Classes="meta" Grid.Row="0" Grid.Column="0" Text="Version" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="0" Grid.Column="0" Text="{loc:Tr modals.about.version}" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="0" Grid.Column="1" Text="{Binding AppVersion}" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="1" Grid.Column="0" Text="Data" VerticalAlignment="Center"/>
<TextBlock Classes="meta" Grid.Row="1" Grid.Column="0" Text="{loc:Tr modals.about.data}" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="1" Grid.Column="1" Text="{Binding DataFolderPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding DataFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="2" Grid.Column="0" Text="Logs" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="{loc:Tr modals.about.open}" Command="{Binding OpenPathCommand}" CommandParameter="{Binding DataFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="2" Grid.Column="0" Text="{loc:Tr modals.about.logs}" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="2" Grid.Column="1" Text="{Binding LogsFolderPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding LogsFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="3" Grid.Column="0" Text="Config" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="{loc:Tr modals.about.open}" Command="{Binding OpenPathCommand}" CommandParameter="{Binding LogsFolderPath}"/>
<TextBlock Classes="meta" Grid.Row="3" Grid.Column="0" Text="{loc:Tr modals.about.config}" VerticalAlignment="Center"/>
<TextBlock Classes="path-mono" Grid.Row="3" Grid.Column="1" Text="{Binding WorkerConfigPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="3" Grid.Column="2" Content="Open" Command="{Binding OpenPathCommand}" CommandParameter="{Binding WorkerConfigPath}"/>
<Button Classes="btn" Grid.Row="3" Grid.Column="2" Content="{loc:Tr modals.about.open}" Command="{Binding OpenPathCommand}" CommandParameter="{Binding WorkerConfigPath}"/>
</Grid>
</ScrollViewer>

View File

@@ -2,9 +2,10 @@
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"
x:Class="ClaudeDo.Ui.Views.Modals.DiffModalView"
x:DataType="vm:DiffModalViewModel"
Title="Diff"
Title="{loc:Tr modals.diff.windowTitle}"
Width="1200" Height="800" MinWidth="700" MinHeight="450"
CanResize="True"
WindowDecorations="BorderOnly"
@@ -48,11 +49,11 @@
</Style>
</Window.Styles>
<ctl:ModalShell Title="DIFF" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Merge" Command="{Binding MergeCommand}"/>
<Button Classes="btn" Content="{loc:Tr modals.diff.merge}" Command="{Binding MergeCommand}"/>
</StackPanel>
</ctl:ModalShell.Footer>

View File

@@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.ListSettingsModalView"
x:DataType="vm:ListSettingsModalViewModel"
Title="List settings"
Title="{loc:Tr modals.listSettings.title}"
Width="520" Height="720"
CanResize="True"
MinWidth="460" MinHeight="520"
@@ -19,14 +20,14 @@
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="LIST SETTINGS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell Title="{loc:Tr modals.listSettings.title}" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
<Button Grid.Column="0" Content="Delete list" Classes="danger"
<Button Grid.Column="0" Content="{loc:Tr modals.listSettings.deleteList}" Classes="danger"
Command="{Binding DeleteCommand}" MinWidth="90"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
<Button Classes="btn" Content="{loc:Tr settings.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{loc:Tr settings.save}" Classes="primary" Command="{Binding SaveCommand}" MinWidth="90"/>
</StackPanel>
</Grid>
</ctl:ModalShell.Footer>
@@ -37,24 +38,24 @@
<!-- GENERAL -->
<StackPanel Spacing="0">
<TextBlock Classes="section-label" Text="GENERAL"/>
<TextBlock Classes="section-label" Text="{loc:Tr modals.listSettings.sectionGeneral}"/>
<Border Classes="section">
<StackPanel Spacing="12">
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Name"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.name}"/>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Working directory"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.workingDirectory}"/>
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="(none)" />
<Button Classes="btn" Grid.Column="1" Content="Browse..." Margin="8,0,0,0" Click="BrowseClicked" />
<TextBox Grid.Column="0" Text="{Binding WorkingDir}" PlaceholderText="{loc:Tr modals.listSettings.workingDirectoryPlaceholder}" />
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr modals.listSettings.browse}" Margin="8,0,0,0" Click="BrowseClicked" />
</Grid>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Default commit type"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/>
<ComboBox ItemsSource="{Binding CommitTypeOptions}"
SelectedItem="{Binding DefaultCommitType, Mode=TwoWay}"
HorizontalAlignment="Left" MinWidth="160" />
@@ -66,28 +67,28 @@
<!-- AGENT -->
<StackPanel Spacing="0">
<Grid ColumnDefinitions="*,Auto" Margin="4,0,0,6">
<TextBlock Classes="section-label" Text="AGENT" Margin="0"/>
<Button Classes="btn" Grid.Column="1" Content="Reset agent settings"
<TextBlock Classes="section-label" Text="{loc:Tr modals.listSettings.sectionAgent}" Margin="0"/>
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr modals.listSettings.resetAgentSettings}"
Command="{Binding ResetAgentSettingsCommand}" />
</Grid>
<Border Classes="section">
<StackPanel Spacing="12">
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Model"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.model}"/>
<ComboBox ItemsSource="{Binding ModelOptions}"
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
HorizontalAlignment="Left" MinWidth="160" />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="System prompt (appended)"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.systemPrompt}"/>
<TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap"
MinHeight="80" />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Agent file"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.agentFile}"/>
<Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0"
ItemsSource="{Binding Agents}"
@@ -102,7 +103,7 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Classes="btn" Grid.Column="1" Content="Browse..."
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr modals.listSettings.browse}"
Margin="8,0,0,0" Click="BrowseAgentClicked" />
</Grid>
<TextBlock Classes="path-mono" Text="{Binding SelectedAgent.Path}"

View File

@@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.MergeModalView"
x:DataType="vm:MergeModalViewModel"
Title="Merge worktree"
Title="{loc:Tr modals.merge.windowTitle}"
Width="560" Height="460" MinWidth="460" MinHeight="360"
CanResize="True"
WindowDecorations="BorderOnly"
@@ -17,12 +18,12 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="MERGE WORKTREE" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell Title="{loc:Tr modals.merge.title}" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Merge" Classes="primary"
<Button Classes="btn" Content="{loc:Tr modals.merge.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{loc:Tr modals.merge.merge}" Classes="primary"
Command="{Binding SubmitCommand}"
IsDefault="True" MinWidth="90"/>
</StackPanel>
@@ -35,19 +36,19 @@
<TextBlock Classes="title" Text="{Binding TaskTitle, StringFormat='Merging: {0}'}" />
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Target branch"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.merge.targetBranch}"/>
<ComboBox ItemsSource="{Binding Branches}"
SelectedItem="{Binding SelectedBranch}"
HorizontalAlignment="Stretch"
IsEnabled="{Binding !IsBusy}" />
</StackPanel>
<CheckBox Content="Remove worktree after merge"
<CheckBox Content="{loc:Tr modals.merge.removeWorktree}"
IsChecked="{Binding RemoveWorktree}"
IsEnabled="{Binding !IsBusy}" />
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Commit message"/>
<TextBlock Classes="field-label" Text="{loc:Tr modals.merge.commitMessage}"/>
<TextBox Text="{Binding CommitMessage}"
AcceptsReturn="True"
TextWrapping="Wrap"
@@ -63,7 +64,7 @@
<Border Classes="danger-box"
IsVisible="{Binding HasConflict}">
<StackPanel Spacing="4">
<TextBlock Classes="title" Text="Conflicted files:" />
<TextBlock Classes="title" Text="{loc:Tr modals.merge.conflictedFiles}" />
<ItemsControl ItemsSource="{Binding ConflictFiles}">
<ItemsControl.ItemTemplate>
<DataTemplate>

View File

@@ -2,9 +2,10 @@
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"
x:Class="ClaudeDo.Ui.Views.Modals.RepoImportModalView"
x:DataType="vm:RepoImportModalViewModel"
Title="Add repos as lists"
Title="{loc:Tr modals.repoImport.windowTitle}"
Width="560" Height="480" MinWidth="420" MinHeight="320"
CanResize="True"
WindowDecorations="BorderOnly"
@@ -16,11 +17,11 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="ADD REPOS AS LISTS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell Title="{loc:Tr modals.repoImport.title}" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"
VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Classes="btn" Content="{loc:Tr modals.repoImport.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{Binding CreateButtonText}" Command="{Binding CreateCommand}"
IsEnabled="{Binding CanCreate}" MinWidth="120" Classes="primary"/>
</StackPanel>
@@ -31,10 +32,10 @@
<!-- Toolbar: search + folder actions -->
<StackPanel DockPanel.Dock="Top" Spacing="8" Margin="20,12,20,6">
<TextBox Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Search repos…"/>
PlaceholderText="{loc:Tr modals.repoImport.searchPlaceholder}"/>
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Classes="btn" Grid.Column="0" Content="Add folder" Click="AddFolderClicked"/>
<Button Classes="btn" Grid.Column="2" Content="Forget folders"
<Button Classes="btn" Grid.Column="0" Content="{loc:Tr modals.repoImport.addFolder}" Click="AddFolderClicked"/>
<Button Classes="btn" Grid.Column="2" Content="{loc:Tr modals.repoImport.forgetFolders}"
Command="{Binding ForgetFoldersCommand}"
IsVisible="{Binding HasFolders}"/>
</Grid>
@@ -55,7 +56,7 @@
VerticalAlignment="Center" Margin="4,0,0,0"/>
<TextBlock Classes="path-mono" Grid.Column="2" Text="{Binding FullPath}"
VerticalAlignment="Center" Margin="8,0,0,0"/>
<TextBlock Classes="meta" Grid.Column="3" Text="(already added)"
<TextBlock Classes="meta" Grid.Column="3" Text="{loc:Tr modals.repoImport.alreadyAdded}"
VerticalAlignment="Center" Margin="8,0,0,0"
IsVisible="{Binding AlreadyAdded}"/>
</Grid>

View File

@@ -3,9 +3,11 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
xmlns:locm="using:ClaudeDo.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
x:DataType="vm:SettingsModalViewModel"
Title="Settings"
Title="{loc:Tr settings.title}"
Width="580" Height="760" MinWidth="480" MinHeight="520"
CanResize="True"
WindowDecorations="BorderOnly"
@@ -19,12 +21,12 @@
</Window.KeyBindings>
<ctl:ModalShell Title="SETTINGS" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell Title="{loc:Tr settings.title}" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="Save" Classes="primary"
<Button Classes="btn" Content="{loc:Tr settings.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
<Button Content="{loc:Tr settings.save}" Classes="primary"
Command="{Binding SaveCommand}"
IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
</StackPanel>
@@ -42,89 +44,117 @@
<TabControl Padding="20,16" TabStripPlacement="Top">
<TabItem Header="General">
<TabItem Header="{loc:Tr settings.tabGeneral}">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Default instructions"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.language}"/>
<ComboBox ItemsSource="{Binding General.Languages}"
SelectedItem="{Binding General.SelectedLanguage, Mode=TwoWay}"
HorizontalAlignment="Left" Width="220">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="locm:LanguageOption">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.defaultInstructions}"/>
<TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="110"
PlaceholderText="Baseline instructions applied to every task"
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*,12,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="field-label" Text="Model"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
<ComboBox ItemsSource="{Binding General.Models}"
SelectedItem="{Binding General.DefaultModel, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Classes="field-label" Text="Max turns"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxTurns}"/>
<NumericUpDown Value="{Binding General.DefaultMaxTurns, Mode=TwoWay}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
</StackPanel>
<StackPanel Grid.Column="4" Spacing="4">
<TextBlock Classes="field-label" Text="Permission"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.permission}"/>
<ComboBox ItemsSource="{Binding General.PermissionModes}"
SelectedItem="{Binding General.DefaultPermissionMode, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Max parallel executions"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.maxParallelExecutions}"/>
<NumericUpDown Value="{Binding General.MaxParallelExecutions, Mode=TwoWay}"
Minimum="1" Maximum="20" Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="140"/>
<TextBlock Text="How many queued tasks the worker runs at once."
<TextBlock Text="{loc:Tr settings.general.maxParallelExecutionsHint}"
Opacity="0.6" FontSize="12"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.standupWeekday}"/>
<ComboBox SelectedIndex="{Binding General.StandupWeekday, Mode=TwoWay}" HorizontalAlignment="Left">
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySunday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdayMonday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdayTuesday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdayWednesday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdayThursday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdayFriday}"/>
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySaturday}"/>
</ComboBox>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Worktrees">
<TabItem Header="{loc:Tr settings.tabWorktrees}">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<Grid ColumnDefinitions="*,12,2*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="field-label" Text="Strategy"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.worktrees.strategy}"/>
<ComboBox ItemsSource="{Binding Worktrees.WorktreeStrategies}"
SelectedItem="{Binding Worktrees.WorktreeStrategy, Mode=TwoWay}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Classes="field-label" Text="Central worktree root"/>
<TextBlock Classes="field-label" Text="{loc:Tr settings.worktrees.centralWorktreeRoot}"/>
<TextBox Text="{Binding Worktrees.CentralWorktreeRoot, Mode=TwoWay}"
PlaceholderText="e.g. C:\worktrees"/>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox IsChecked="{Binding Worktrees.WorktreeAutoCleanupEnabled, Mode=TwoWay}"
Content="Auto-cleanup finished worktrees after"
Content="{loc:Tr settings.worktrees.autoCleanup}"
VerticalAlignment="Center"/>
<NumericUpDown Value="{Binding Worktrees.WorktreeAutoCleanupDays, Mode=TwoWay}"
Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0"
IsEnabled="{Binding Worktrees.WorktreeAutoCleanupEnabled}"/>
<TextBlock Classes="body" Text="days" VerticalAlignment="Center"/>
<TextBlock Classes="body" Text="{loc:Tr settings.worktrees.days}" VerticalAlignment="Center"/>
</StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
<StackPanel Spacing="8">
<Button Classes="btn" Content="Cleanup finished worktrees"
<Button Classes="btn" Content="{loc:Tr settings.worktrees.cleanupFinished}"
Command="{Binding Worktrees.CleanupWorktreesCommand}"
HorizontalAlignment="Left"/>
<StackPanel>
<Button Content="Force-remove all worktrees" Classes="danger"
<Button Content="{loc:Tr settings.worktrees.forceRemoveAll}" Classes="danger"
Command="{Binding Worktrees.RequestResetConfirmCommand}"
HorizontalAlignment="Left"
IsVisible="{Binding !Worktrees.ShowResetConfirm}"/>
<Border Classes="danger-box"
IsVisible="{Binding Worktrees.ShowResetConfirm}">
<StackPanel Spacing="8">
<TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost."
<TextBlock Text="{loc:Tr settings.worktrees.confirmRemoveAll}"
Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="btn" Content="Cancel" Command="{Binding Worktrees.CancelResetConfirmCommand}"/>
<Button Content="Remove All" Classes="danger"
<Button Classes="btn" Content="{loc:Tr settings.cancel}" Command="{Binding Worktrees.CancelResetConfirmCommand}"/>
<Button Content="{loc:Tr settings.worktrees.removeAll}" Classes="danger"
Command="{Binding Worktrees.ConfirmResetAllCommand}"/>
</StackPanel>
</StackPanel>
@@ -137,32 +167,32 @@
</ScrollViewer>
</TabItem>
<TabItem Header="Files">
<TabItem Header="{loc:Tr settings.tabFiles}">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="AGENTS"/>
<TextBlock Classes="meta" Text="Restore bundled default agents. Existing files are not overwritten."
<TextBlock Classes="section-label" Text="{loc:Tr settings.files.agentsSection}"/>
<TextBlock Classes="meta" Text="{loc:Tr settings.files.agentsHint}"
TextWrapping="Wrap"/>
<Button Classes="btn" Content="Restore default agents"
<Button Classes="btn" Content="{loc:Tr settings.files.restoreDefaultAgents}"
Command="{Binding Files.RestoreDefaultAgentsCommand}"
IsEnabled="{Binding !Files.IsBusy}"
HorizontalAlignment="Left"/>
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="PROMPTS"/>
<TextBlock Classes="section-label" Text="{loc:Tr settings.files.promptsSection}"/>
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="System" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.systemPrompt}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="0" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="0" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="System"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Planning" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.planningPrompt}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Planning"/>
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Agent" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.agentPrompt}" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.AgentPromptPath}" VerticalAlignment="Center"/>
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="Open in editor"
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Agent"/>
</Grid>
</StackPanel>
@@ -172,11 +202,11 @@
</ScrollViewer>
</TabItem>
<TabItem Header="Prime Claude">
<TabItem Header="{loc:Tr settings.tabPrime}">
<ScrollViewer>
<StackPanel Spacing="12" Margin="0,8,0,0">
<TextBlock Classes="meta" TextWrapping="Wrap"
Text="Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately."/>
Text="{loc:Tr settings.prime.description}"/>
<ItemsControl ItemsSource="{Binding Prime.Rows}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="settings:PrimeScheduleRowViewModel">
@@ -186,13 +216,13 @@
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
<ToggleButton Classes="day-toggle" Content="Mo" IsChecked="{Binding Monday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="Tu" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="We" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="Th" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="Fr" IsChecked="{Binding Friday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="Sa" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="Su" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTu}" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayWe}" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTh}" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayFr}" IsChecked="{Binding Friday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
</StackPanel>
<TimePicker Grid.Column="2"
SelectedTime="{Binding TimeOfDay, Mode=TwoWay}"
@@ -208,7 +238,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Classes="btn" Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
<Button Classes="btn" Content="{loc:Tr settings.prime.addSchedule}" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
</StackPanel>
</ScrollViewer>
</TabItem>

View File

@@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.UnfinishedPlanningModalView"
x:DataType="vm:UnfinishedPlanningModalViewModel"
Title="Unfinished planning session"
Title="{loc:Tr modals.unfinishedPlanning.windowTitle}"
Width="440" Height="200"
CanResize="False"
WindowDecorations="None"
@@ -16,13 +17,13 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="UNFINISHED PLANNING SESSION" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell Title="{loc:Tr modals.unfinishedPlanning.title}" CloseCommand="{Binding CancelCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="Discard" Command="{Binding DiscardCommand}" MinWidth="80"/>
<Button Classes="btn" Content="Finalize" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
<Button Content="Resume" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
<Button Classes="btn" Content="{loc:Tr modals.unfinishedPlanning.discard}" Command="{Binding DiscardCommand}" MinWidth="80"/>
<Button Classes="btn" Content="{loc:Tr modals.unfinishedPlanning.finalize}" Command="{Binding FinalizeNowCommand}" MinWidth="80"/>
<Button Content="{loc:Tr modals.unfinishedPlanning.resume}" Command="{Binding ResumeCommand}" Classes="primary" MinWidth="80"/>
</StackPanel>
</ctl:ModalShell.Footer>
@@ -32,7 +33,7 @@
TextTrimming="CharacterEllipsis"/>
<TextBlock Classes="body">
<Run Text="{Binding DraftCount}"/>
<Run Text=" draft task(s) waiting to be finalized."/>
<Run Text="{loc:Tr modals.unfinishedPlanning.draftTasksSuffix}"/>
</TextBlock>
</StackPanel>

View File

@@ -0,0 +1,43 @@
<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"
x:Class="ClaudeDo.Ui.Views.Modals.WeeklyReportModalView"
x:DataType="vm:WeeklyReportModalViewModel"
Title="{loc:Tr modals.weeklyReport.windowTitle}"
Width="820" Height="640"
WindowDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr modals.weeklyReport.title}" CloseCommand="{Binding CloseCommand}">
<DockPanel Margin="20,16">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8">
<TextBlock Classes="meta" Text="{loc:Tr modals.weeklyReport.from}" VerticalAlignment="Center"/>
<ctl:ThemedDatePicker SelectedDate="{Binding StartDate}"/>
<TextBlock Classes="meta" Text="{loc:Tr modals.weeklyReport.to}" VerticalAlignment="Center"/>
<ctl:ThemedDatePicker SelectedDate="{Binding EndDate}"/>
<Button Classes="btn" Content="{loc:Tr modals.weeklyReport.generate}" Command="{Binding GenerateCommand}"
IsVisible="{Binding EmptyStateVisible}"/>
<Button Classes="btn" Content="{loc:Tr modals.weeklyReport.regenerate}" Command="{Binding GenerateCommand}"
IsVisible="{Binding HasReport}"/>
</StackPanel>
<TextBlock DockPanel.Dock="Top" Classes="meta" Margin="0,8,0,0"
Text="{Binding StatusMessage}"/>
<TextBlock DockPanel.Dock="Top" Classes="meta" Margin="0,16"
Text="{loc:Tr modals.weeklyReport.emptyStateHint}"
IsVisible="{Binding EmptyStateVisible}"/>
<ScrollViewer IsVisible="{Binding HasReport}">
<ctl:MarkdownView Markdown="{Binding ReportMarkdown}"/>
</ScrollViewer>
</DockPanel>
</ctl:ModalShell>
</Window>

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