Eine List-Handler-ConPTY-Session, die mitten in einer Phase stirbt, war bisher
ein stilles totes Tile: InteractiveTerminalViewModel.OnSessionProcessExited
setzte nur bei einem Tod innerhalb des 5s-Grace-Fensters ein sichtbares Banner.
Neu:
- HandlerRoundState (JSON unter ~/.claudeDo/handler-rounds.json, keine
EF-Migration): Phase + Task-Set der laufenden Runde, geschrieben von
BuildForMergeHelperAsync ("triage") und BuildForMergeHelperHandoffAsync.
Ohne das existieren beide nur im transienten HandoffRequested-Broadcast.
- HandlerRestartCoordinator: entscheidet den Neustart, fail-closed. Beleg fuer
"war das Usage-Limit" ist FiveHour.Utilization >= 95 zum Todeszeitpunkt --
eine ConPTY-Session hat kein stdout, TaskRunner.IsUsageLimitMessage hat hier
nichts zu lesen. Nur bei PowerUse (beide Keys), max ein Neustart pro Runde,
Warten bis FiveHour.ResetsAt, dann HandoffRequested erneut broadcasten.
Verbraucht kein Handoff-Budget (HandoffRoundTracker.Register wird nicht
angefasst). WorkerLog-Zeilen beim Tod und beim Neustart.
- Ui meldet den Exit-Code (nur dort bekannt) via
WorkerHub.ReportInteractiveSessionExited, ohne selbst zu filtern.
Sibling zu UsageLimitAutoContinueCoordinator, kein Umbau davon: der sieht nur
headless Runs.
Kollision mit #264 (Handoff-Budget im Brief sichtbar machen): beide Seiten haben
HandoffRoundTracker, HandoffMcpTools und InteractiveLaunchSpecService angefasst.
Der Konflikt war inhaltlich, nicht nur textuell. #264 liest den Cap an zwei
Stellen aus der Konstante (GetStatus und das roundInfo-Rendering), waehrend
dieser Branch ihn zum Parameter aus app_settings gemacht hat. Ein reiner
Text-Merge haette kompiliert und trotzdem gelogen: Register haette den
konfigurierten Cap durchgesetzt, waehrend der Brief dem Modell weiter "2 Runden
uebrig" gemeldet haette, obwohl es 39 hat.
Aufgeloest so, dass der Cap eine einzige Quelle hat:
- GetStatus(taskId, maxHandoffs) nimmt den Cap als Parameter, mit demselben
Clamp wie Register. Die Rechnerei liegt damit an einer Stelle.
- GetHandoffStatus liest das Setting und gibt es weiter.
- Das roundInfo-Rendering benutzt GetStatus statt eigener Arithmetik.
Zwei Tests nageln genau diesen Drift fest, damit er beim naechsten Merge nicht
unbemerkt zurueckkommt: einer auf GetHandoffStatus, einer auf den Brief.
MaxHandoffs -> DefaultMaxHandoffs in #264s vier Test-Referenzen; die laufen alle
mit Default-Settings, die Semantik bleibt.
Der Toggle sitzt unter den Gauges im Usage-Monitor, weil "das restliche Fenster
ausnutzen" eine Entscheidung ist, die man beim Blick auf das restliche Fenster
trifft. Er speichert sofort per Read-modify-write, gleiche Disziplin wie
SaveStagesAsync, damit er keine Felder ueberschreibt, die er nicht besitzt.
Zwei Stellen, die das Opt-in still ausgeschaltet haetten:
- WorkerHub.UpdateListConfig loescht die Config-Zeile, wenn alle Felder leer
sind. PowerUseAllowed muss in diese Leer-Pruefung, sonst verschwindet das
Opt-in sofort wieder, wenn es das einzige gesetzte Feld ist.
- SettingsModalViewModel baut das AppSettingsDto positional neu auf. Ohne
Round-trip haette jedes Speichern der Settings PowerUseEnabled auf den
Record-Default false zurueckgesetzt. Gleiches Muster wie _throttleStages.
Das Handoff-Budget ist als Spinner in Settings -> General editierbar, weil es
auch fuer normale (interaktive) Handler-Runs gilt und nicht an PowerUse haengt.
Das Addendum selbst ist ueber Settings -> Files anpassbar.
Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte
tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies
ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das
alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder
pickbar). BlockedByTaskId bleibt unberuehrt.
- Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id
verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird
- QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten
wird blockiert, alle Kanten muessen done sein, eine Kante auf einen
verschwundenen Task blockiert weiter
- SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine
BFS ueber den Graphen, faengt also auch A->B->C->A
- blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von
ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt)
- add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt
als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig
- UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label,
die lineare Kette rendert unveraendert
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs enthält ~107 handgeschriebene OnPropertyChanged(nameof(...))-Aufrufe in partial void OnXChanged-Methoden. Das Projekt nutzt an anderen Stellen den Generator: [NotifyPropertyChangedFor] in DetailsIslandViewModel.cs, ConflictResolverViewModel.cs, AgentConfigEditorViewModel.cs, Installer/WizardViewModel.cs.
Das ist die eine Datei, in der eine ve
ClaudeDo-Task: 87976462-5845-4e01-8233-32afadca02bd
"New list" pre-creates the DB row before opening List Settings.
Cancelling or closing that modal left an untitled empty list behind;
CreateListAsync now rolls the row back unless the modal reports Saved.
Also moves the new-list/import-repo buttons up next to the "My Lists"
label instead of a row below the list, and puts Notes above Important
in the smart-list nav order.
Reuses the existing drag-drop move (MoveTaskToListAsync) so the same
running/worktree guards apply — only ListId changes.
Also count WaitingForReview tasks (not Done-with-a-branch) for the
review portion of the status pill, and localize the pill and open-task
count strings that were still hardcoded English.
The banner compared the running worker's build SHA against the
selected list's HEAD via a git ancestor check on every list switch
and reconnect. It added a git-rev-parse MSBuild target, a hub call,
and a poll loop for a corner case that hasn't come up in practice —
not worth the moving parts.
The status PATCH needs statusName alongside statusId or the API 400s,
so every write-back had been silently failing. The board's flat
assigneeName is null on every live ticket; the real assignee list is
ticketAssignees[]. Import now also picks up In Bearbeitung tickets,
not just Offen ones.
Both issues are live-verified against the real API, so the feature
flag flips back on.
Shift+Enter did nothing because AcceptsReturn was only set from a style
setter and never reached the box. Setting it on the element is half the
fix: TextBox.OnKeyDown consumes Enter regardless of modifiers and runs
before any bubbling handler, so the split chain would have died in its
place. Enter is now taken in the tunnel phase and Shift+Enter is left to
the box.
Alongside, the notes list stops borrowing furniture it has no use for:
no "0 open tasks" subtitle, no show-completed and list-settings buttons,
and no details pane beside it — the editor spans that column instead of
leaving it empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor was reachable from one button that only appeared in My Day,
which is how it got forgotten. It is now a permanent sidebar entry next
to My Day and Planned, and it fills the tasks island rather than the
details pane, because a note list is content.
Inside, the date picker is gone: today sits at the top with the capture
box, older days follow as dated groups. Enter in a note saves it and
opens the next one, Shift+Enter breaks the line so notes can be several,
Backspace on an empty note deletes it and steps back up. Deleting also
has a visible hover button instead of only "clear the text and hope".
Agent-written notes carry a marker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Listen bekommen eine Icon-Spalte (Migration AddListIcon) und einen Icon-Picker
in den Listeneinstellungen; 34 kuratierte Geometrien aus der Icon-Bibliothek,
Fallback auf "Folder" bei unbekanntem Key.
Der Metadata-Footer im Task-Detail entfällt; created-at wandert in die
Header-Zeile neben das ID-Badge, der Close-Button (und CloseDetailsCommand)
fällt weg.
- "+" öffnet ein Menü mit allen Listen (working dir) statt direkt den
Ordner-Dialog; "Ordner wählen…" bleibt als Fallback erste Zeile, damit
das Menü auch bei fehlgeschlagener Listen-Abfrage nutzbar bleibt
- Rail-Rows: Kontextmenü "Sitzung öffnen" (gesperrt für Running — der
Worktree gehört dort dem autonomen Lauf)
- Aktivitäts-Strip statt reiner Queue: WaitingForReview/Failed/Roadblock
kommen dazu, Attention zuerst, unterschieden nur über Farbe (Aktionen
bleiben in der Task-Detailansicht)
ScopeGlobs auf Tasks und SerializeOnFileOverlap auf der Listen-Config waren
ungenutzt: der Scope wurde nie befuellt, also hat der Queue-Picker nie
serialisiert. ScopeOverlap, das Picker-Gate, die DTO-Felder, die UI-Option und
die Spalten fallen weg (Migration DropFileScopeSerialization).
Die Bandel-API ist unter dem eingetragenen Hostnamen nicht erreichbar
(Verbindung aktiv abgelehnt), das Feature bliebe also eine Einrichtung,
die nicht funktionieren kann. TicketSettingsTabViewModel.FeatureEnabled
versteckt alle drei Einstiegspunkte: Settings-Tab, Projekt-Dropdown in
den Listen-Einstellungen, Kontextmenue-Eintrag.
Gespeicherte Base-URL, PAT und Projektverknuepfungen bleiben erhalten
und greifen beim Umlegen sofort wieder.
Der Footer-Strip trug schon immer ein Level, es gab nur keinen
Einstiegspunkt jenseits von FlashFooterError. "3 neue Tasks" in Rot
liest sich als Fehlschlag.
Bisher nur per Hand in worker.config.json. Neuer Abschnitt CLAUDE CLI mit
eigenem Save-Button (die Datei gehoert dem Worker, nicht app_settings) und einer
Zeile, wohin der Wert per ExecutableResolver tatsaechlich aufloest — 'nicht im
PATH gefunden' war bisher erst am fehlgeschlagenen Run zu sehen. WorkerConfig ist
DI-Singleton und ClaudeProcess loest pro Spawn auf, also greift die Aenderung ab
dem naechsten Run ohne Neustart.
SaveOnlineInbox/SaveClaudeBin teilen jetzt ein SaveKey(), damit beide dieselbe
read-modify-write-Semantik haben (alle anderen Keys bleiben unberuehrt).
Die zugehoerigen Locale-Keys sind im vorigen Commit mitgelaufen.
Die vier UsageThrottle-Prozente waren nur per Gauge-Drag im Usage-Monitor
erreichbar und wurden im Settings-Modal unsichtbar durchgeschleift. Jetzt
read-only angezeigt plus Button in den Usage-Monitor. Der Button schliesst die
Settings absichtlich: Save() schreibt die beim Oeffnen gelesenen Throttle-Werte
zurueck und wuerde ein zwischenzeitliches Draggen ueberschreiben.
Bisher gab es nur AppSettings.DefaultPermissionMode global — ein Task, der plan
oder acceptEdits braucht, erzwang das Umstellen der globalen Einstellung. Neue
Spalten tasks.permission_mode und list_config.permission_mode, Auflösung
task -> list -> global im EffectiveRunConfigResolver (den TaskRunner und
get_effective_run_config gemeinsam nutzen), ComboBox mit Inherited-Badge im
geteilten Agent-Editor.
MigrationBaselineTests: das Fixture baute per EnsureCreated das heutige Schema
und stempelte Legacy-History darauf — jede Migration nach dem Squash lief damit
in 'duplicate column name'. Es migriert jetzt gezielt bis InitialCreate und
prueft 'nichts pending' statt 'InitialCreate ist die einzige Zeile'.
Das Flag war bisher nur ueber MCP set_list_config erreichbar. UpdateListConfigDto
fuehrt es tri-state (null = gespeicherten Wert behalten), damit nur das
Listen-Modal es setzen/loeschen kann und kein anderer Aufrufer es per Omission
verliert.
Neuer Button in der virtuellen Review-Liste: öffnet das Merge-Helper-Auswahlmodal
im Review-Modus (alle WaitingForReview-Tasks repo-verknüpfter Listen, gruppiert
unter Listen-Headern, alle vorgetickt) und startet pro gewählter Liste eine
ConPTY-Handler-Session direkt in der Merge-Phase — via bestehendem Handoff-Spec
(nextPhase "merge"), kein Triage/Wait davor. Handler-Host-Task wie beim vollen
"Let Claude handle it"-Lauf (HandlerBaseCommit, IsManual).
Delete (row context menu + detail header) now gates on HasInteractiveSession
the same way SendToQueue already does, so an open ConPTY session's claude
process can't be orphaned by deleting its task out from under it.
Row removal (in-UI delete and the delta-refresh path for externally deleted
tasks) now clears SelectedTask via SelectFrom(null, "row-removed") when the
removed row was selected, so the detail pane no longer keeps showing a
deleted task with live buttons.
The DB's global UtcConverter only tags DateTimes as Utc on read
(SpecifyKind), it never converts on write. SetScheduledForAsync persisted
the ThemedDatePicker's Local/Unspecified wall-clock value verbatim, so
QueuePicker's comparison against DateTime.UtcNow fired scheduled tasks late
by the local UTC offset (e.g. 2h in CEST). Convert to UTC at the write
boundary, and ToLocalTime() at the read/compare sites (overdue checks in
TaskRowViewModel/TasksIslandViewModel, the date-picker's edit seed) so
existing scheduled/overdue display doesn't shift.
Existing DB rows hold local wall-clock values mistagged as Utc; no
migration added (few rows, one-time 2h-class shift accepted per the
originating audit finding).
update_task_status Idle/Done, WorkerHub.SetTaskStatus, and the UI's Mark
Done/Cancelled menu items could all force a Running task's status without
going through TaskStateService's guards, leaving its CLI process untracked
and its worktree writes silently dropped. Idle now routes through the
already-guarded TaskStateService.ResetToIdleAsync instead of the repo's
unconditional ResetToManualAsync; Done and SetTaskStatus explicitly reject
Running; the context menu disables both entries with a tooltip while
running. TaskRunner also now logs a warning whenever a terminal transition
(SubmitForChildren/SubmitForReview/Complete/Fail) is rejected instead of
silently discarding the run result. DeleteTask's MCP path gets the same
friendly foreign-key message WorkerHub.DeleteTask already had for a task
with children.
Nine interfaces had exactly one implementation and no test double — they existed
only to be named twice in a DI registration: IFindingsStore, IFindingsStoreLocator,
IPrimeScheduleSignal, IRefineRunner, IWeekReportService, IMergeCoordinator,
IMissionControlPane, IOnlineLoginService, ITaskListFilter. Consumers now depend on
the concrete type; the DTO records that shared those files moved next to their
implementation. IInteractiveLaunchSpecService stays — it carries 54 lines of
contract documentation, which is not ceremony.
IMergeCoordinator in particular had a redundant null object: MergeCoordinator with
a null Handler already no-ops, and every test used the real class with Handler set.
Filtering/ collapses from 8 files to 1. ITaskListFilter and TaskListFilterBase were
a double abstraction over four predicates, with MatchesAsContext => false declared
in both. SmartFlagFilter also compiled its expression twice (its own _flag plus the
inherited Matches cache) — it now uses the cache.
StaticTokenAuthProvider was in src but production uses ZitadelAuthProvider; it is
a test double, so it moves to the test project. Its own test goes away with it.
Eight public members had no caller anywhere in src: GitService.GetFileDiffAsync,
SubtaskRepository.DeleteByTaskIdAsync, TaskRepository.GetByListAsync (a
backwards-compat alias for GetByListIdAsync) and .GetByCreatorAsync,
WorktreeRepository.GetByStatesAsync, TaskMonitorViewModel.SetPendingQuestion
(a duplicate of the live-event lambda), PrimeClaudeTabViewModel.ApplyFiredEvent,
and StreamLineFormatter.FormatFile. TaskAttachmentRepository.DeleteAllForTaskAsync
was reachable only from its own test; the ON DELETE CASCADE on task_attachments
already covers it. Tests for the deleted members go with them.
Dropped two package refs the platform already provides: EntityFrameworkCore.Design
in Worker (the design-time factory and the migrations live in Data, which has its
own ref) and System.IO.FileSystem.AccessControl in Installer.Tests (net8.0-windows
ships the ACL APIs in the shared framework).
The modal deliberately shows only a slice of the analytics; this hands off
to TokenTracker's own local dashboard for the rest. The worker starts
`tokentracker serve` on demand and returns the URL, the UI opens the browser.
Three things the spawn has to get right: port 7680 is not free on Windows
(Delivery Optimization holds [::]:7680) and serve does not fall back, so we
scan 7680-7689 with a dual-stack bind probe; --no-open because the CLI would
open the browser before the server answers; and the child is a cmd.exe shim,
so shutdown kills the process tree. --no-sync keeps our no-cloud-sync rule.
ListsIslandViewModel.OpenInExplorer, MergeSectionViewModel.OpenWorktree,
WorktreesOverviewModalViewModel.OpenInExplorer, AboutModalViewModel.OpenPath and
TasksIslandViewModel.OpenTaskWorktree each reimplemented "open this path in the
shell" with their own existence check, launch mechanism and error handling.
Replace all five with the new ShellOpen.Path helper and route failures through
the existing ErrorReported -> footer-strip convention instead of bare catch {}.
SendToQueue, Cancel, Refine, Planning and OpenWorktree each had a CanX property
and a hand-written negation of it for the DisabledReason tooltip, kept in sync
only by a comment's promise. Replace each pair with a private gate method
returning (Can, Reason) so there is one source of truth per condition, and add
a state-matrix test pinning down the CanX == (Reason == null) invariant that
the old comment only asserted.
ListsIslandViewModel and SettingsModalViewModel each re-implemented "does
this list have a linked WorkingDir" with different whitespace handling.
RepoLinkage.IsLinked/IsLinkedInDb is now the single definition; both
callers derive from it, closing the whitespace-only WorkingDir gap where
the Settings modal disagreed with the ListsIsland banner.
Both viewmodels carried byte-identical copies of the reset-countdown
formatter. Moved it to UsageTimeFormat.FormatRemaining and pointed
both call sites at it; locale keys and output unchanged.
Zwei liegengebliebene Consumer aus den letzten beiden Commits:
- TaskRunner klassifiziert seit 07dd7570 "usage_limit", aber weder
TaskRowViewModel.FailureReasonLabel noch vm.failureReason (de/en) noch
die get_task-Tool-Beschreibung kannten den Wert — die UI zeigte
"Grund unbekannt", das MCP-Doc listete weiterhin max_turns|timeout|error.
- TaskRowViewModel.CanOpenWorktree prüfte nur auf einen nicht-leeren
String. Die Zeile behält den Path eines gemergten/verworfenen Worktrees,
also war der Menüpunkt aktiv und Process.Start warf in den Footer.
Jetzt zusätzlich Directory.Exists — dieselbe Prüfung, die
WorktreesOverviewModalViewModel und MergeSectionViewModel schon machen.
Klassifiziert einen echten Usage-Limit-Abbruch als eigene FailureReason
"usage_limit" (TaskRunner.ClassifyFailureReason: nur bei terminal_reason
"api_error" plus einem Limit-Muster im gerenderten Fehlertext, nicht an
Status==Failed allein). Neuer Toggle AutoContinueOnUsageLimit (app_settings,
Default aus) unter Settings → General → "Usage limit stop":
- UsageLimitAutoContinueCoordinator feuert pro Task genau einmal ContinueTask
über OverrideSlotService, sobald das 5h-Fenster (UsageState.Snapshot.FiveHour
.ResetsAt) tatsächlich zurückgesetzt ist; ein persistenter Marker
(TaskEntity.UsageLimitAutoContinuedAt) verhindert einen zweiten Anlauf bei
einem erneuten Limit-Treffer.
- QueueService schedult zusätzlich einen exakten Wake-Timer auf den
Reset-Zeitpunkt, statt nur auf den 30s-Backstop zu warten.
- Fail-open durchgängig: kein Snapshot/keine Reset-Zeit → kein Timer, kein
Continue, kein Throw. Toggle aus ändert das heutige Verhalten nicht.
Migration AddUsageLimitAutoContinue fügt beide Spalten hinzu; die von
`dotnet ef migrations add` mitgescaffoldete leere UpdateData auf app_settings
(columns/values: []) erzeugte ungültiges SQL ("near WHERE") und wurde entfernt
— TaskNumberMigrationTests deckte das über den vollen Migrate()-Pfad auf.
Closing the main window used to silently kill every open Mission Control
ConPTY session (App's ShutdownMode.OnMainWindowClose tears the process down
without warning). Intercept Window.Closing, and when at least one pane is
open, ask via the existing ConfirmAsync pattern before proceeding; cancelling
leaves the window and sessions alive, confirming force-closes as before.
Neither the initial nor the handoff kickoff ever told a list-handler
session its own handler task id, so handoff_list_handler(taskId, ...)
was unrenderable -- the handoff chain broke exactly where it was needed
(#200/#201 on 2026-08-21).
Add {handlerTaskId} to both MergeHelperInitialDefault and
MergeHelperHandoffDefault, thread a handlerTaskId parameter through
BuildForMergeHelperAsync (interface, WorkerHub.GetMergeHelperLaunchSpec,
IWorkerClient/WorkerClient, and the MissionControlViewModel call site,
which already had the id from CreateMergeHelperTaskAsync but never
passed it on), and render it in BuildForMergeHelperHandoffAsync from the
taskId parameter it already receives.
RenderTemplate leaves unknown/missing tokens untouched, so a
user-edited override without the new token still renders fine -- no
forced migration for override users.
Group 1 of the task-row context menu gets a fourth gated entry, right
after "Open interactive session": it opens the task's worktree folder
via Process.Start/UseShellExecute, mirroring ListsIslandViewModel's
OpenInExplorer. Missing worktree greys the item out with a tooltip
reason instead of hiding it, keeping Group 1's fixed length. Failures
report through TasksIslandViewModel.ErrorReported (footer strip), not
a silent catch.
The ctor-time LoadAsync races WorkerClient.StartAsync, so the SignalR
call fails silently and the pill only fills on the next worker poll
(up to 900s). Subscribe to ConnectionRestoredEvent, which also fires
on the first successful connect, to retry the cached snapshot.
HasNoLinkedRepo on ListsIslandViewModel drives a dismiss-less banner under the
Lists-Island header when no User list has a linked repo; recomputed on load
and after list CRUD / list-settings save. Sharpens the #192 empty-state texts
into one readable chain (banner → no-repo list → repo-linked list) and adds a
line to the WorkConsole review gate spelling out that Approve also merges the
whole unit.
TabControl bleibt Content-Host, TabStrip wird retempliert (nur
PART_SelectedContentHost) und durch eine zweigruppige Sidebar
(BASIS/ERWEITERT) ersetzt. General wird in Allgemein/Ausfuehrung/Berichte
gesplittet, Session Skills (Checkbox-Liste aus General + Skills-Tab)
zu einer Liste zusammengelegt, und ein Repo-Hinweisstreifen erscheint
auf Worktrees/Prime Claude/Session Skills/Berichte solange keine Liste
ein WorkingDir hat. Fensterbreite 580 -> 700.
Gruppe 1 (Send to queue, Remove from queue, Cancel execution, Open quick
session, Refine task) ist jetzt fix sichtbar und gated per IsEnabled + Grund
statt komplett auszublenden. Planning/Schedule wandern in Untermenüs (Mark as
existierte bereits); der Planning-Kopf zeigt einen Grund, wenn er sonst leer
wäre. Refine-Button, ungesetzter Star und Dequeue-X erscheinen nur bei
Hover-oder-Selektion (TaskRowViewModel.ShowRowActions); ein gesetzter Star und
der Refining-Spinner bleiben immer sichtbar. Der Branch-Chip wandert aus der
Zeile in eine Meta-Zeile in TaskHeaderBar.
A3 headless verifiziert: Avalonia 12 zeigt ToolTips auf IsEnabled=false
Controls nur mit ToolTip.ShowOnDisabled="True" (Default ist false) — dieses
Attached Property existiert bereits und wird für die neuen Disabled-Reason-
Tooltips genutzt.