- .gitignore for .NET output, IDE files, runtime artifacts (todo.db, worktrees, sandbox, logs) - docs/plan.md: .NET worker spawning Claude CLI, per-list working_dir + git worktrees, 3NF schema, SignalR IPC, Windows-Service deployment path
22 KiB
ToDo-App mit autonomem Agent-Worker — Design
Context
Ziel: eine persönliche ToDo-App als Desktop-Anwendung, in der mehrere Listen verwaltet werden können. Ein Teil der Tasks soll autonom von Claude abgearbeitet werden (z.B. Recherche, Code-Aufgaben, Notizen-Verarbeitung). Die Autonomie läuft in einem getrennten Hintergrund-Prozess, damit die UI davon entkoppelt bleibt.
Motivation: Aufgaben "parken" und später von einem Agenten abarbeiten lassen, ohne dafür manuell einen Chat zu öffnen. Ergebnisse landen wieder an der Task, sodass die Liste die Single-Source-of-Truth bleibt.
Listen können ein eigenes working_dir (z.B. ein Projekt-Repo) haben. Tasks solcher Listen laufen in einem dedizierten Git-Worktree, der Worker commitet die Änderungen am Ende automatisch. Die UI zeigt Branch + Diff und erlaubt Merge/Keep/Discard.
Architektur-Überblick
Monorepo (C:\Private\ClaudeDo, gehostet auf git.kuns.dev), zwei Prozesse, gekoppelt über eine gemeinsame SQLite-Datenbank:
┌─────────────────────┐ ┌──────────────────────────────┐
│ UI (Avalonia .NET) │ │ Worker (.NET 8 Console) │
│ Listen, Tasks, │◄──────►│ BackgroundService │
│ Diff/Merge-Dialog │ SQLite │ spawnt: claude -p ... │
└─────────────────────┘ WAL │ cwd = Worktree der Task │
│ └──────────┬───────────────────┘
│ │ stdout (ndjson)
└─────── todo.db ────────────────┘
(~/.todo-app/todo.db)
Ziel-Repo (z.B. LagerApp):
C:\Private\LagerApp\ (main repo, unangetastet)
C:\Private\.claudedo-worktrees\<list>\<task-id>\ (Worktree pro Task)
- UI (
ClaudeDo.Ui, Avalonia MVVM): Listen & Tasks verwalten, Ergebnisse + Diffs anzeigen, Worktree-Aktionen. - Worker (
ClaudeDo.Worker, .NET 8 Console mit Kestrel + SignalR): hostet einen SignalR-Hub auf localhost, verwaltet die Queue intern, spawnt Claude CLI als Child-Prozess pro Task, streamt Events live an die UI, commitet, persistiert Ergebnisse. - SQLite im WAL-Mode für persistente Daten (Listen, Tasks, Tags, Worktrees). Keine Koordinations-Felder — Live-Status läuft über SignalR.
- SignalR über localhost-HTTP (Default-Port
47821, konfigurierbar) als Push-Channel zwischen Worker und UI. UI-Connection-State ⇒ "Worker online".
Datenmodell
Schema in 3NF. Keine Mehrwert-Felder (z.B. JSON-Arrays), keine transitiven Abhängigkeiten: Worktree-Attribute wandern in eine eigene Tabelle, Tags sind über Junctions modelliert, Worker-Slots sind eigene Rows.
lists
idTEXT PK (uuid)nameTEXT NOT NULLcreated_atTIMESTAMP NOT NULLworking_dirTEXT NULL — absoluter Pfad. Gesetzt + Git-Repo → Worktree-Modus.default_commit_typeTEXT NOT NULL DEFAULT'chore'
tasks
idTEXT PK (uuid)list_idTEXT NOT NULL REFERENCESlists(id)ON DELETE CASCADEtitleTEXT NOT NULLdescriptionTEXT NULLstatusTEXT NOT NULL —manual|queued|running|done|failed(runningbleibt persistiert für Crash-Recovery: stalerunning-Tasks werden beim Worker-Start auffailedgesetzt)scheduled_forTIMESTAMP NULL — "nicht vor"resultTEXT NULL (Markdown)log_pathTEXT NULL — Pfad zur ndjson-Log-Dateicreated_atTIMESTAMP NOT NULLstarted_atTIMESTAMP NULLfinished_atTIMESTAMP NULLcommit_typeTEXT NOT NULL DEFAULT'chore'— Conventional-Commit-Prefix (wird beim Anlegen auslist.default_commit_typebefüllt, danach unabhängig).
tags
idINTEGER PK AUTOINCREMENTnameTEXT NOT NULL UNIQUE — z.B.agent,manual,code,research
list_tags (Junction, Liste hat 0..n Tags)
list_idTEXT NOT NULL REFERENCESlists(id)ON DELETE CASCADEtag_idINTEGER NOT NULL REFERENCEStags(id)ON DELETE CASCADE- PK
(list_id, tag_id)
task_tags (Junction, Task hat 0..n Tags; überschreibt die List-Tags nicht automatisch — die Vereinigung aus list_tags ∪ task_tags bildet die effektive Tag-Menge der Task; Ausschluss über noch zu spezifizierenden Negations-Mechanismus, bis dahin additiv)
task_idTEXT NOT NULL REFERENCEStasks(id)ON DELETE CASCADEtag_idINTEGER NOT NULL REFERENCEStags(id)ON DELETE CASCADE- PK
(task_id, tag_id)
worktrees (1:1 mit Tasks, die im Worktree-Modus laufen)
task_idTEXT PK REFERENCEStasks(id)ON DELETE CASCADE — Task hat höchstens einen Worktree.pathTEXT NOT NULL — absoluter Pfad zum Worktree.branch_nameTEXT NOT NULL — z.B.claudedo/<task-id-kurz>.base_commitTEXT NOT NULL — SHA desworking_dir-HEAD beim Anlegen.head_commitTEXT NULL — SHA nach Auto-Commit (NULL bis Commit erfolgt).diff_statTEXT NULL — Output vongit diff --stat base..head.stateTEXT NOT NULL DEFAULT'active'—active|merged|discarded|kept.created_atTIMESTAMP NOT NULL
Die Abwesenheit einer worktrees-Zeile entspricht dem alten Wert 'none'. Non-Worktree-Tasks (Liste ohne working_dir) erzeugen keine Zeile.
Hinweis: Es gibt keine worker_heartbeat- oder worker_slots-Tabelle. Worker-Online-Status und aktive Slots laufen über SignalR (siehe Sektion "IPC").
Tag-Modell (Startset)
- Tags leben in
tags, Zuordnung vialist_tagsundtask_tags(Junctions). - Effektive Tag-Menge einer Task =
list_tags(task.list_id) ∪ task_tags(task.id)(additiv). - Minimal-Startset in
tags:manual→ Worker ignoriert, reine Notiz/Checkliste.agent→ Worker pickt auf, wenn Statusqueued.
- Weitere Profile (
code,research…) später als zusätzliche Rows intags+ Handler im Worker.
IPC (SignalR)
Worker hostet Microsoft.AspNetCore.SignalR über Kestrel auf http://127.0.0.1:<port>/hub (Default 47821, in worker.config.json überschreibbar). Bindung nur an Loopback. Kein Auth-Layer.
Hub: WorkerHub
Server-Methoden (UI ruft auf):
GetActive()→[{ slot: "queue"|"override", taskId, startedAt }]— initialer State-Sync nach Connect.RunNow(taskId)→ triggert sofortigen Override-Run; wirft, wenn Override-Slot belegt oder Task nicht existiert.CancelTask(taskId)→ killt laufenden CLI-Child-Prozess der Task.WakeQueue()→ Worker prüft Queue sofort (für instant pickup nach Task-Anlage).Ping()→"pong"+ Worker-Version (Sanity-Check).
Client-Methoden (Worker pusht an UI):
TaskStarted(slot, taskId, startedAt)TaskFinished(slot, taskId, status, finishedAt)— Statusdone|failed.TaskMessage(taskId, ndjsonLine)— streamt jedes Claude-Event live (für Live-Log in TaskDetail).WorktreeUpdated(taskId)— Auto-Commit fertig, UI lädtworktrees-Row neu.TaskUpdated(taskId)— generisches Signal: UI lädt Task neu (z.B. nach Result-Persist).
Connection-State der UI ⇒ "Worker online" (kein Heartbeat in DB nötig).
UI-Verhalten bei offline Worker:
- StatusBar: "Worker offline".
- "Run Now" deaktiviert (RPC nicht möglich).
- Task-Anlage funktioniert weiter (DB-only); wird beim nächsten Worker-Start aufgenommen.
Queue-Semantik
- Default: Tasks mit Tag
agentund Statusqueuedwerden sequenziell abgearbeitet (FIFO, nachcreated_at). Worker-interner In-Memory-Slot_queueSlot. - Schedule:
scheduled_forgesetzt → Worker überspringt, bis Zeit erreicht. - Override: UI ruft
RunNow(taskId)per SignalR. Worker startet die Task in_overrideSlotparallel zur Queue. Max. 1 Queue + 1 Override = 2 gleichzeitige Runs. - Zweiter
RunNowwährend Override-Slot belegt → SignalR-Methode wirft, UI zeigt "Override bereits aktiv". - Wake-up: Bei Task-Anlage ruft die UI
WakeQueue(), damit der Worker nicht erst auf den nächsten Tick warten muss.
Worker-Komponenten (.NET)
Projekt: ClaudeDo.Worker — Microsoft.NET.Sdk.Web Console-App (für Kestrel + SignalR), Teil von ClaudeDo.sln, referenziert ClaudeDo.Data.
Program.cs—WebApplication.CreateBuilder+AddSignalR()+AddHostedService<QueueService>().app.MapHub<WorkerHub>("/hub")auf konfiguriertem Loopback-Port. Bindung:app.Urls.Add($"http://127.0.0.1:{cfg.Port}").Hub/WorkerHub.cs— implementiert die in der IPC-Sektion beschriebenen Methoden. DelegiertRunNow/CancelTask/WakeQueueanQueueService.GetActive()liest aus den In-Memory-Slots.Hub/HubBroadcaster.cs— Wrapper umIHubContext<WorkerHub>mit typisiertenTaskStarted/TaskFinished/TaskMessage/WorktreeUpdated/TaskUpdated-Methoden. Wird inTaskRunnerundClaudeProcessinjiziert.Services/QueueService.cs—BackgroundService, interner Timer (z.B. 30s als Backstop, primärer Trigger istWakeQueue). Wählt nächste Task (Tagagentviatask_tags/list_tags,scheduled_for ≤ now,_queueSlotfrei). Hält In-Memory-Slots_queueSlot,_overrideSlot. SendetTaskStarted/TaskFinishedüberHubBroadcaster.Services/StaleTaskRecovery.cs— beim Start: alletasks.status='running'→'failed', Begründung "worker restart".Runner/TaskRunner.cs— orchestriert eine Task-Ausführung (siehe Lifecycle unten).Runner/WorktreeManager.cs— kapseltgit worktree add/remove,git rev-parse,git diff --stat,git add -A && git commit.Runner/ClaudeProcess.cs— kapseltSystem.Diagnostics.Process:- Cmd:
claude -p --output-format stream-json --verbose --dangerously-skip-permissions WorkingDirectory = worktreePath(bzw. Sandbox-Dir bei Listen ohneworking_dir).- Prompt über stdin (vermeidet Quoting-Probleme mit Zeilenumbrüchen).
- Async stdout-Reader, jede Zeile = 1 JSON-Event → an
LogWriter,MessageParserundHubBroadcaster.TaskMessage(taskId, line). - stderr in Log mit
[stderr]-Prefix. - Finales
type:"result"→.result-Feld extrahieren (Markdown). - Exit != 0 oder fehlendes Result → Task
failed, Fehler insresult-Feld.
- Cmd:
Runner/LogWriter.cs— streamt ndjson nach~/.todo-app/logs/<task-id>.ndjson.Runner/CommitMessageBuilder.cs— baut Conventional-Commit-Message (siehe unten).Config/WorkerConfig.cs— lädt~/.todo-app/worker.config.json:Kein API-Key — CLI nutzt die bestehende User-Session.{ "db_path": "~/.todo-app/todo.db", "sandbox_root": "~/.todo-app/sandbox", "log_root": "~/.todo-app/logs", "worktree_root_strategy": "sibling", "central_worktree_root": "~/.todo-app/worktrees", "queue_backstop_interval_ms": 30000, "signalr_port": 47821, "claude_bin": "claude" }
Dependencies: Microsoft.AspNetCore.App (Kestrel + SignalR via Microsoft.NET.Sdk.Web), Microsoft.Extensions.Hosting, Microsoft.Data.Sqlite (via ClaudeDo.Data), System.Text.Json.
Task-Lifecycle (Worktree-Modus)
Nur wenn list.working_dir gesetzt und ein Git-Repo ist:
- Worktree anlegen (
WorktreeManager.CreateAsync):base = git -C <working_dir> rev-parse HEADbranch = claudedo/<task-id-kurz>worktreePath = <working_dir>/../.claudedo-worktrees/<list-slug>/<task-id>/git -C <working_dir> worktree add -b <branch> <worktreePath> <base>- Insert in
worktrees:task_id,path,branch_name,base_commit,state='active',created_at=now.
- Claude-Run via
ClaudeProcess.RunAsync(prompt, worktreePath, ...). - Auto-Commit (
WorktreeManager.CommitAsync), nur wenn Run erfolgreich UND Änderungen vorhanden:git -C <worktreePath> add -A- Nichts staged → skippen (
head = base). - Sonst:
git commit -m <conventional-message>(Author aus User-Git-Config). head = rev-parse HEAD,diff_stat = git diff --stat base..head.- Update
worktrees:head_commit,diff_stat.
- Status finalisieren:
tasks.status='done',tasks.result= Claude-Result-Markdown,tasks.finished_at=now. - Kein Auto-Cleanup.
worktrees.statebleibt'active', bis der User über die UI eine Aktion wählt.
Fehlerpfade:
- Run failed / Exit != 0 →
tasks.status='failed', kein Auto-Commit,worktrees-Row bleibt mitstate='active'zur Inspektion. working_dirist kein Git-Repo → Task failed mit klarer Meldung; keineworktrees-Row, keine Fallback-Sandbox.- Worktree-Anlegen failed (Branch/Dir existiert) → Task failed; keine
worktrees-Row geschrieben; Runner räumt nicht auf.
Non-Worktree-Modus (list.working_dir NULL): cwd = ~/.todo-app/sandbox/<task-id>/, kein Git, Standard-Toolset wie bisher.
Conventional Commit Message
Format: {type}({list-slug}): {title}
type=task.commit_type(Defaultchore).{list-slug}=list.namelowercased, whitespace→-, nicht-alphanumerisch entfernt.{title}=task.title, truncated auf 60 Zeichen.- Body (optional, zweizeilig nach Leerzeile):
{task.description, max 400 chars} ClaudeDo-Task: <task-id>
Beispiel: feat(lager-app): add barcode scan retry logic
UI-Komponenten (Avalonia)
- MainWindow mit 2-Pane-Layout: links Listen, rechts Tasks der gewählten Liste.
- ListEditor — Feld
Working Directory(Ordner-Picker), DefaultCommit TypeDropdown. - TaskItemView zeigt Titel, Tags, Status (Badge), "Run Now"-Button (→ ruft
WorkerHub.RunNow(taskId); deaktiviert wenn Worker offline). - TaskDetailPane — Description, Result-Markdown (AvaloniaEdit oder Markdown.Avalonia), Link zum ndjson-Log.
- Bei vorhandener
worktrees-Row mitstate='active': Branch,diff_stat, Buttons Open Worktree, Show Diff, Merge into main, Keep as branch, Discard. - Merge into main:
git -C <working_dir> merge --ff-only <branch>(UI warnt bei non-ff) →worktrees.state='merged', Worktree + Branch entfernen. - Keep as branch: Worktree entfernen, Branch behalten →
worktrees.state='kept'. - Discard:
git worktree remove --force+git branch -D→worktrees.state='discarded'.
- Bei vorhandener
- TaskEditor — Dropdown
Commit Type(Default aus Liste). - StatusBar — Worker-Status aus SignalR-Connection-State (Connected ⇒ online), aktive Tasks aus In-Memory-State (initial via
GetActive(), danach viaTaskStarted/TaskFinished-Events). - TaskDetailPane zeigt bei laufender Task den Live-Stream der
TaskMessage-Events (rolling Log). - WorkerClient (
ClaudeDo.Ui/Services/WorkerClient.cs) — kapseltHubConnection, Auto-Reconnect, exponiertIObservable<…>-Events bzw.INotifyPropertyChanged-Properties für die ViewModels. - SettingsDialog — Default-Tags, Pfad zur todo.db, SignalR-Port (muss zum Worker passen).
DB-Zugriff via Microsoft.Data.Sqlite + Repository-Layer (TaskRepository, ListRepository). Git-Operationen (UI + Worker) über gemeinsamen GitService in ClaudeDo.Data. MVVM via CommunityToolkit.Mvvm.
Worker als Windows-Service (Ziel-Deployment)
Initial läuft der Worker als Console-Prozess (lokales Dev-Setup). Im Endzustand soll er als Windows-Service automatisch starten.
Code-seitig:
- Paket
Microsoft.Extensions.Hosting.WindowsServicesreferenzieren. - In
Program.cs:builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker"). - Logging zusätzlich über
EventLog(builder.Logging.AddEventLog(...)), damit Service-Fehler im Windows Event Viewer landen. - Alle Pfade in
worker.config.jsonabsolut auflösen (%USERPROFILE%/~expandieren) — der Service-Working-Directory ist standardmäßigC:\Windows\System32. StaleTaskRecovery(siehe oben) sorgt nach Service-Restart automatisch für das Aufräumen hängenderrunning-Tasks.- Restart-Verhalten via
sc.exe failure-Konfig oder beim Install.
Install:
- Veröffentlichen mit
dotnet publish -c Release -r win-x64 --self-contained false. - Service registrieren:
sc.exe create ClaudeDoWorker binPath= "C:\Path\To\ClaudeDo.Worker.exe" start= auto sc.exe failure ClaudeDoWorker reset= 60 actions= restart/5000/restart/10000/restart/30000 - Später optional: kleines
ClaudeDo.Installer-Projekt (WiX oder MSIX), das das auch macht.
Auth-Konflikt mit "User-CLI-Session" beachten:
Der Worker-Service läuft per Default unter LocalSystem — der hat keinen Zugriff auf die ~/.claude/-Session des interaktiven Users, in der der CLI-Login liegt. Optionen:
- Empfohlen: Service unter dem User-Account laufen lassen (
sc.exe config ClaudeDoWorker obj= ".\<username>" password= "..."oder viaservices.msc→ "Log On As"). Dann greift die bestehendeclaude login-Session des Users. Voraussetzung: User-Account hat das Recht "Log on as a service". - Fallback: Wieder auf API-Key wechseln (
ANTHROPIC_API_KEYals Umgebungsvariable des Service oder imworker.config.json). Dann ist der Service unabhängig vom User-Profil — verliert aber den Vorteil "kein Key-Handling".
Entscheidung wird beim Service-Deployment getroffen, bleibt für die initiale Console-Variante irrelevant. Service-Modus erfordert keine Schema- oder API-Änderungen am Worker.
SignalR im Service-Modus: Bindung bleibt 127.0.0.1:47821. Da die UI auf demselben Rechner läuft, ist Loopback-Erreichbarkeit gegeben — Windows-Firewall greift bei Loopback nicht.
Project-Layout (Monorepo)
Repo: ClaudeDo auf git.kuns.dev, lokal C:\Private\ClaudeDo
/ClaudeDo
ClaudeDo.sln
/src
/ClaudeDo.App Avalonia Entry (App.axaml, Program.cs)
/ClaudeDo.Ui Views, ViewModels, Worktree-Dialoge
/ClaudeDo.Data Repositories, Models, SqliteConnectionFactory, GitService
/ClaudeDo.Worker Console/BackgroundService
/tests
/ClaudeDo.Worker.Tests xUnit, In-Memory-SQLite + temp-Repo für Worktree-Tests
/schema
schema.sql Single source of truth für DB-Schema
migrations/ (später, falls nötig)
/docs
plan.md, architecture.md
.gitignore bin/, obj/, *.db, worker.config.json, logs/
Vorteil Monorepo: gemeinsames schema.sql, atomische Änderungen über UI+Worker, ein Clone reicht, einheitlicher .NET-Stack.
Verification
- Schema-Init: Worker erstellt
todo.dbbei erstem Start mit WAL, legt alle Tabellen an (lists,tasks,tags,list_tags,task_tags,worktrees). Initial werden intagsdie Rows'agent'und'manual'eingefügt. →sqlite3 todo.db ".schema"zeigt die erwartete Struktur in 3NF. 1a. SignalR-Endpoint: Worker startet,curl -i http://127.0.0.1:47821/hubantwortet (HTTP 400 Negotiate-Fehler ohne SignalR-Handshake ist OK — Hauptsache Port lauscht und nur auf Loopback). 1b. Hub-Roundtrip (UI- oder Test-Client):connection.InvokeAsync<string>("Ping")liefert"pong". - CLI-Preflight:
claude --versionläuft, User eingeloggt. Worker-Startup-Check failed laut, wenn nicht erfüllt. - Smoke-Spawn (Unit-Test):
claude -p --output-format stream-json --dangerously-skip-permissions, Prompt"ping"via stdin →result-Message erhalten + geparst. - End-to-End Happy Path (Non-Worktree):
- UI: Liste "Test" (kein
working_dir), Task mit Tagagent+ Statusqueued, Description "Schreibe eine Haiku über Intralogistik". - Worker erkennt Task binnen 5s (Log:
picked up task <id>), startet CLI-Run, schreibt Result zurück. - UI zeigt Status
done+ Result-Markdown.
- UI: Liste "Test" (kein
- Worktree-Happy-Path (Integrations-Test mit temp-Repo):
- Liste mit
working_dir=<temp-repo>, Task "add hello.txt with content hi". - Nach Run: Worktree unter
<temp-repo>/../.claudedo-worktrees/<slug>/<id>/,hello.txtdrin, 1 Commitchore(<slug>): add hello.txtauf Branchclaudedo/<id-kurz>.worktrees-Row für die Task:head_commit != base_commit,diff_statgesetzt,state='active'.
- Liste mit
- No-Changes-Run: Prompt, der nichts ändert →
tasks.status='done', kein Commit,worktrees.head_commit IS NULL(oder =base_commit). - Kein Git-Repo:
working_dirauf normalen Ordner → Task failed mit klarer Meldung, keineworktrees-Row. - Merge-UI: nach erfolgreichem Run Button "Merge into main" →
<working_dir>HEAD enthält den Commit, Branch + Worktree weg,worktrees.state='merged'. - Override-Parallelität: Zwei
queuedTasks → nur eine läuft. Bei zweiter "Run Now" klicken →WorkerHub.RunNowsucceeded, beide laufen parallel, UI bekommt zweiTaskStarted-Events (slotsqueue+override), in Worktree-Listen zwei getrennte Worktrees. DritterRunNow→ SignalR-Exception "override slot busy". - Schedule: Task mit
scheduled_for = now + 2min→ Worker wartet; nach Ablauf startet sie. - Worker-Offline-Erkennung: Worker-Prozess killen → SignalR-Connection bricht ab, UI-StatusBar wechselt sofort auf "offline" (kein Polling-Lag). "Run Now"-Buttons werden disabled. Laufende Task bleibt
running→ beim nächsten Worker-Start setztStaleTaskRecoverysie auffailed,worktrees-Row bleibt zur Inspektion. UI reconnected automatisch und ruft erneutGetActive()für den State-Sync. - Live-Stream: Während eine Task läuft, UI öffnet TaskDetailPane → ndjson-Events erscheinen in Echtzeit (jedes
TaskMessage-Event wird angehängt). - Wake-up: Task in UI anlegen mit Tag
agent, Statusqueued→ UI ruftWakeQueue()→ Worker startet die Task in < 1s (nicht erst nach Backstop-Intervall). - Unit-Tests (Worker):
QueueService-Selection-Logik (Override-Slot-Belegung, Schedule-Filter, Sequenzialität) mit In-Memory-SQLite. Hub-Methoden mitTestServer+HubConnectionBuilder.
Offene Punkte für später (nicht Scope dieses Plans)
- Zusätzliche Tag-Profile mit spezialisierten Toolsets (
code→ engere Permissions,research→ Netzwerk). - MCP-Server-Integration für externe Dienste.
- Notification bei Task-fertig (Windows Toast).
- Non-ff Merges (Rebase-/Squash-Option im UI).
- Bulk-Discard alter Worktrees.
- Anzeige der ndjson-Message-Chronik im UI.
- Windows Job Objects für garantierten Child-Cleanup beim Worker-Crash.
- Installer-Projekt (
ClaudeDo.Installer, WiX/MSIX), das den Service registriert + UI shortcut anlegt.