feat(ui): Cheat-Sheet-Fenster im Hilfe-Menue
Sechs Markdown-Sheets (git, Conventional Commits, Claude CLI, Claude-Config, dotnet CLI, PowerShell) als EmbeddedResource, gerendert im modelosen, per Topmost anpinnbaren CheatSheetsWindow. MarkdownView bekommt dafuer das Opt-in CommandRows: `befehl` - beschreibung wird zu Mono-Chip + Copy-Button, jede ##-Sektion landet in einem Expander. Enthaelt ausserdem: Mission Control wird beim Oeffnen aus dem Minimiert-Zustand geholt, nicht nur aktiviert.
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
# Claude CLI
|
||||||
|
|
||||||
|
## Session starten
|
||||||
|
- `claude` — interaktive Session im aktuellen Verzeichnis
|
||||||
|
- `claude "<prompt>"` — Session mit erstem Prompt starten
|
||||||
|
- `claude -c` — letzte Session in diesem Verzeichnis fortsetzen
|
||||||
|
- `claude -r` — Session aus einer Auswahlliste fortsetzen
|
||||||
|
- `claude -r <session-id>` — bestimmte Session fortsetzen
|
||||||
|
- `claude --fork-session -r <id>` — Session unter neuer ID fortsetzen
|
||||||
|
- `claude -n "<name>"` — Anzeigename für die Session setzen
|
||||||
|
|
||||||
|
## Modell und Aufwand
|
||||||
|
- `claude --model sonnet` — Alias oder voller Name (`fable`, `opus`, `sonnet`, `claude-opus-5`)
|
||||||
|
- `claude --effort high` — Denkaufwand: `low`, `medium`, `high`, `xhigh`, `max`
|
||||||
|
- `claude --fallback-model sonnet` — Ausweichmodell bei Überlast (nur mit `-p`)
|
||||||
|
|
||||||
|
## Nicht interaktiv
|
||||||
|
- `claude -p "<prompt>"` — antworten und beenden
|
||||||
|
- `claude -p --output-format json` — ein JSON-Ergebnis
|
||||||
|
- `claude -p --output-format stream-json` — Streaming-JSON für Skripte
|
||||||
|
- `claude -p --include-partial-messages` — Teil-Chunks mitstreamen
|
||||||
|
- `claude -p --max-budget-usd 5` — Kostendeckel für den Lauf
|
||||||
|
- `claude -p --json-schema '<schema>'` — strukturierte Ausgabe validieren
|
||||||
|
|
||||||
|
## Berechtigungen und Tools
|
||||||
|
- `claude --permission-mode <mode>` — `acceptEdits`, `auto`, `plan`, `manual`, `dontAsk`, `bypassPermissions`
|
||||||
|
- `claude --allowed-tools "Bash(git *)" Edit` — nur diese Tools erlauben
|
||||||
|
- `claude --disallowed-tools "Bash(rm *)"` — diese Tools sperren
|
||||||
|
- `claude --tools "Bash,Edit,Read"` — Built-in-Toolset festlegen
|
||||||
|
- `claude --add-dir <pfad>` — weiteres Arbeitsverzeichnis freigeben
|
||||||
|
- `claude --restricted` — ohne befehlsausführende Tools und ohne lokale Settings
|
||||||
|
|
||||||
|
## Kontext und Konfiguration
|
||||||
|
- `claude --append-system-prompt "<text>"` — Systemprompt ergänzen
|
||||||
|
- `claude --system-prompt "<text>"` — Systemprompt ersetzen
|
||||||
|
- `claude --settings <datei|json>` — zusätzliche Settings laden
|
||||||
|
- `claude --agents '<json>'` — eigene Subagents für diese Session
|
||||||
|
- `claude --mcp-config <datei>` — MCP-Server aus JSON laden
|
||||||
|
- `claude --strict-mcp-config` — nur diese MCP-Server, alle anderen ignorieren
|
||||||
|
- `claude --safe-mode` — alle Anpassungen aus, zum Fehlersuchen
|
||||||
|
- `claude --bare` — Minimalmodus ohne Hooks, Plugins, CLAUDE.md
|
||||||
|
|
||||||
|
## Hintergrund-Sessions
|
||||||
|
- `claude --bg "<prompt>"` — im Hintergrund starten, gibt die Kurz-ID aus
|
||||||
|
- `claude agents` — laufende Hintergrund-Sessions auflisten
|
||||||
|
- `claude attach <id>` — Hintergrund-Session in dieses Terminal holen
|
||||||
|
- `claude logs <id>` — letzte Ausgabe einer Hintergrund-Session
|
||||||
|
- `claude stop <id>` — Session stoppen, Verlauf bleibt
|
||||||
|
- `claude rm <id>` — Session löschen
|
||||||
|
|
||||||
|
## Worktree
|
||||||
|
- `claude -w` — neuen Git-Worktree für diese Session anlegen
|
||||||
|
- `claude -w <name>` — Worktree mit eigenem Namen
|
||||||
|
|
||||||
|
## Verwaltung
|
||||||
|
- `claude doctor` — Installation prüfen
|
||||||
|
- `claude update` — auf neue Version aktualisieren
|
||||||
|
- `claude mcp` — MCP-Server konfigurieren
|
||||||
|
- `claude plugin` — Plugins verwalten
|
||||||
|
- `claude auth` — Authentifizierung verwalten
|
||||||
|
- `claude setup-token` — Langzeit-Token einrichten
|
||||||
|
- `claude -d api,hooks` — Debugmodus mit Kategoriefilter
|
||||||
|
- `claude -v` — Version ausgeben
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Claude Konfiguration
|
||||||
|
|
||||||
|
## Dateien und Orte
|
||||||
|
- `~/.claude/settings.json` — globale Settings für alle Projekte
|
||||||
|
- `.claude/settings.json` — Projekt-Settings, eingecheckt fürs Team
|
||||||
|
- `.claude/settings.local.json` — persönliche Projekt-Settings, nicht eingecheckt
|
||||||
|
- `Rangfolge: managed > local > projekt > global` — spezifischer gewinnt
|
||||||
|
- `/config` — Settings interaktiv in der Session ansehen und ändern
|
||||||
|
- `claude doctor` — Konfiguration auf Fehler prüfen
|
||||||
|
|
||||||
|
## CLAUDE.md
|
||||||
|
- `~/.claude/CLAUDE.md` — globale Anweisungen für alle Projekte
|
||||||
|
- `CLAUDE.md` — im Projekt-Root, wird bei Start automatisch geladen
|
||||||
|
- `<unterordner>/CLAUDE.md` — wird geladen, sobald dort gearbeitet wird
|
||||||
|
- `@pfad/zur/datei.md` — Import einer weiteren Datei in eine CLAUDE.md
|
||||||
|
- `# <notiz>` — in der Session: Notiz in die CLAUDE.md übernehmen
|
||||||
|
- `/init` — CLAUDE.md für ein Projekt generieren lassen
|
||||||
|
|
||||||
|
## Berechtigungen
|
||||||
|
- `"permissions": { "allow": [...], "deny": [...], "ask": [...] }` — Struktur in settings.json
|
||||||
|
- `Bash(git *)` — Bash-Befehle per Muster erlauben oder sperren
|
||||||
|
- `Edit(src/**)` — Dateizugriff per Glob einschränken
|
||||||
|
- `WebFetch(domain:example.com)` — Webzugriff auf eine Domain begrenzen
|
||||||
|
- `mcp__<server>__<tool>` — Regel für ein einzelnes MCP-Tool
|
||||||
|
- `/permissions` — Regeln in der Session ansehen und ergänzen
|
||||||
|
|
||||||
|
## MCP-Server
|
||||||
|
- `claude mcp add <name> -- <befehl>` — lokalen stdio-Server anlegen
|
||||||
|
- `claude mcp add --transport http <name> <url>` — HTTP-Server anlegen
|
||||||
|
- `claude mcp add --scope project <name> ...` — in `.mcp.json` fürs Team
|
||||||
|
- `claude mcp list` — konfigurierte Server anzeigen
|
||||||
|
- `claude mcp remove <name>` — Server entfernen
|
||||||
|
- `/mcp` — Serverstatus in der Session, OAuth-Login
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
- `"hooks": { "PreToolUse": [...] }` — Shell-Befehle an Lifecycle-Events hängen
|
||||||
|
- `PreToolUse / PostToolUse` — vor und nach jedem Tool-Aufruf
|
||||||
|
- `UserPromptSubmit` — beim Absenden eines Prompts
|
||||||
|
- `SessionStart / Stop` — bei Sessionbeginn und -ende
|
||||||
|
- `Exit-Code 2` — blockiert die Aktion, stderr geht an Claude
|
||||||
|
- `/hooks` — Hooks interaktiv verwalten
|
||||||
|
|
||||||
|
## Erweiterungen
|
||||||
|
- `.claude/commands/<name>.md` — eigener Slash-Command `/name`
|
||||||
|
- `.claude/skills/<name>/SKILL.md` — Skill, wird bei passender Aufgabe geladen
|
||||||
|
- `.claude/agents/<name>.md` — eigener Subagent-Typ
|
||||||
|
- `~/.claude/...` — dieselben Ordner global statt pro Projekt
|
||||||
|
- `claude plugin` — Plugins installieren und verwalten
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Conventional Commits
|
||||||
|
|
||||||
|
## Format
|
||||||
|
- `<type>(<scope>): <beschreibung>` — Grundform, Scope optional
|
||||||
|
- `feat(auth): add login endpoint` — Beispiel mit Scope
|
||||||
|
- `fix: prevent crash on empty input` — Beispiel ohne Scope
|
||||||
|
- `imperativ, klein, kein Punkt am Ende` — Stil der Beschreibung
|
||||||
|
|
||||||
|
## Types
|
||||||
|
- `feat` — neues Feature
|
||||||
|
- `fix` — Bugfix
|
||||||
|
- `refactor` — Umbau ohne Verhaltensänderung
|
||||||
|
- `perf` — Performance-Verbesserung
|
||||||
|
- `docs` — nur Dokumentation
|
||||||
|
- `test` — Tests hinzufügen oder korrigieren
|
||||||
|
- `style` — Formatierung, kein Code-Effekt
|
||||||
|
- `build` — Build-System oder Dependencies
|
||||||
|
- `ci` — CI-Konfiguration
|
||||||
|
- `chore` — Sonstiges ohne src- oder Test-Änderung
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
- `feat(api)!: remove legacy endpoint` — `!` markiert einen Breaking Change
|
||||||
|
- `BREAKING CHANGE: <erklärung>` — alternativ als Footer im Commit-Body
|
||||||
|
|
||||||
|
## Body und Footer
|
||||||
|
- `Leerzeile nach der Betreffzeile` — dann optionaler Body mit dem Warum
|
||||||
|
- `Refs: #123` — Footer für Ticket-Referenzen
|
||||||
|
- `Reverts: <hash>` — Footer für Reverts
|
||||||
|
|
||||||
|
## Warum das Ganze
|
||||||
|
- `feat` — löst ein Minor-Release aus (SemVer)
|
||||||
|
- `fix` — löst ein Patch-Release aus (SemVer)
|
||||||
|
- `!` oder `BREAKING CHANGE` — löst ein Major-Release aus
|
||||||
|
- `git log --oneline --grep "^feat"` — Verlauf wird filterbar
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# dotnet CLI
|
||||||
|
|
||||||
|
## Projekt anlegen
|
||||||
|
- `dotnet new list` — verfügbare Vorlagen anzeigen
|
||||||
|
- `dotnet new console -n <name>` — Projekt aus Vorlage anlegen
|
||||||
|
- `dotnet new sln` — Solution anlegen
|
||||||
|
- `dotnet sln add <projekt>` — Projekt in die Solution aufnehmen
|
||||||
|
|
||||||
|
## Bauen
|
||||||
|
- `dotnet restore` — NuGet-Pakete auflösen
|
||||||
|
- `dotnet build <projekt> -c Release` — Release bauen
|
||||||
|
- `dotnet build --no-restore` — ohne Restore, wenn Pakete aktuell sind
|
||||||
|
- `dotnet build -v minimal` — knappere Ausgabe
|
||||||
|
- `dotnet clean -c Release` — Build-Ausgabe löschen
|
||||||
|
|
||||||
|
## Laufen lassen
|
||||||
|
- `dotnet run --project <projekt> -c Release` — starten
|
||||||
|
- `dotnet run --project <projekt> -- <args>` — Argumente an die App
|
||||||
|
- `dotnet watch --project <projekt> run` — bei Dateiänderung neu starten
|
||||||
|
|
||||||
|
## Testen
|
||||||
|
- `dotnet test <projekt> -c Release --no-build` — Tests ohne Neubau
|
||||||
|
- `dotnet test --filter "FullyQualifiedName~<name>"` — nur passende Tests
|
||||||
|
- `dotnet test --filter "FullyQualifiedName!~<name>"` — Tests ausschließen
|
||||||
|
- `dotnet test --logger "console;verbosity=detailed"` — Details pro Test
|
||||||
|
- `dotnet test --blame-hang-timeout 60s` — hängende Tests aufspüren
|
||||||
|
|
||||||
|
## Pakete
|
||||||
|
- `dotnet add <projekt> package <name>` — Paket hinzufügen
|
||||||
|
- `dotnet add <projekt> package <name> --version 1.2.3` — feste Version
|
||||||
|
- `dotnet remove <projekt> package <name>` — Paket entfernen
|
||||||
|
- `dotnet list <projekt> package --outdated` — veraltete Pakete
|
||||||
|
- `dotnet nuget locals all --clear` — NuGet-Caches leeren
|
||||||
|
|
||||||
|
## EF Core Migrationen
|
||||||
|
- `dotnet ef migrations add <name> -p <daten-projekt>` — Migration anlegen
|
||||||
|
- `dotnet ef migrations list -p <daten-projekt>` — Migrationen anzeigen
|
||||||
|
- `dotnet ef migrations remove -p <daten-projekt>` — letzte Migration zurücknehmen
|
||||||
|
- `dotnet ef database update -p <daten-projekt>` — Schema anwenden
|
||||||
|
|
||||||
|
## Veröffentlichen
|
||||||
|
- `dotnet publish <projekt> -c Release -r win-x64 --self-contained` — eigenständiges Build
|
||||||
|
- `dotnet publish <projekt> -c Release -p:PublishSingleFile=true` — eine Exe
|
||||||
|
|
||||||
|
## Diagnose
|
||||||
|
- `dotnet --info` — SDKs, Runtimes, RID
|
||||||
|
- `dotnet --list-sdks` — installierte SDKs
|
||||||
|
- `dotnet format <projekt>` — Formatierung anwenden
|
||||||
|
- `dotnet build -p:TreatWarningsAsErrors=true` — Warnungen als Fehler
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Git
|
||||||
|
|
||||||
|
## Start
|
||||||
|
- `git clone <url>` — Repository klonen
|
||||||
|
- `git init` — neues Repository anlegen
|
||||||
|
- `git remote add origin <url>` — Remote verbinden
|
||||||
|
|
||||||
|
## Stand ansehen
|
||||||
|
- `git status -sb` — Kurzstatus mit Branch-Zeile
|
||||||
|
- `git diff` — nicht gestagete Änderungen
|
||||||
|
- `git diff --staged` — gestagete Änderungen
|
||||||
|
- `git diff main...HEAD` — alles, was dieser Branch gegenüber main bringt
|
||||||
|
- `git diff --stat` — nur Datei- und Zeilenzähler
|
||||||
|
- `git log --oneline --graph --decorate -20` — kompakter Verlauf
|
||||||
|
- `git log -p -- <pfad>` — Verlauf einer Datei mit Diffs
|
||||||
|
- `git show <commit>` — ein Commit im Detail
|
||||||
|
|
||||||
|
## Branch
|
||||||
|
- `git switch <branch>` — Branch wechseln
|
||||||
|
- `git switch -c <name>` — Branch anlegen und wechseln
|
||||||
|
- `git switch -` — zurück zum vorherigen Branch
|
||||||
|
- `git branch -vv` — Branches mit Upstream und letztem Commit
|
||||||
|
- `git branch -d <name>` — gemergten Branch löschen (`-D` erzwingt)
|
||||||
|
|
||||||
|
## Committen
|
||||||
|
- `git add <pfad>` — Änderungen stagen
|
||||||
|
- `git add -p` — Änderungen hunk-weise stagen
|
||||||
|
- `git commit -m "<text>"` — gestagete Änderungen committen
|
||||||
|
- `git commit -- <pfad>` — nur diese Pfade committen
|
||||||
|
- `git commit --amend --no-edit` — letzten Commit um die Staging-Area erweitern
|
||||||
|
- `git commit --fixup <commit>` — Fixup für späteres Autosquash
|
||||||
|
|
||||||
|
## Remote
|
||||||
|
- `git fetch --all --prune` — alles holen, gelöschte Remote-Branches aufräumen
|
||||||
|
- `git pull --rebase` — holen und eigene Commits obendrauf setzen
|
||||||
|
- `git push -u origin <name>` — pushen und Upstream setzen
|
||||||
|
- `git push --force-with-lease` — sicheres Force-Push, nie plain `--force`
|
||||||
|
|
||||||
|
## Rebase und Merge
|
||||||
|
- `git rebase main` — Branch auf aktuellen main setzen
|
||||||
|
- `git rebase -i --autosquash main` — interaktiv aufräumen
|
||||||
|
- `git rebase --continue` — nach gelöstem Konflikt weiter
|
||||||
|
- `git rebase --abort` — Rebase abbrechen, Ausgangszustand zurück
|
||||||
|
- `git merge --no-ff <branch>` — Merge-Commit erzwingen
|
||||||
|
- `git merge --abort` — laufenden Merge abbrechen
|
||||||
|
- `git cherry-pick <commit>` — einzelnen Commit übernehmen
|
||||||
|
|
||||||
|
## Konflikte
|
||||||
|
- `git diff --name-only --diff-filter=U` — konfliktbehaftete Dateien
|
||||||
|
- `git checkout --ours <pfad>` — eigene Seite übernehmen
|
||||||
|
- `git checkout --theirs <pfad>` — fremde Seite übernehmen
|
||||||
|
- `git mergetool` — konfiguriertes Merge-Tool starten
|
||||||
|
|
||||||
|
## Rückgängig
|
||||||
|
- `git restore <pfad>` — Datei auf HEAD zurücksetzen, Änderungen weg
|
||||||
|
- `git restore --staged <pfad>` — unstagen, Änderungen behalten
|
||||||
|
- `git reset --soft HEAD~1` — letzten Commit auflösen, Änderungen gestaged behalten
|
||||||
|
- `git reset --hard <commit>` — alles verwerfen, nicht rückholbar
|
||||||
|
- `git revert <commit>` — Commit durch Gegen-Commit aufheben
|
||||||
|
- `git reflog` — Referenzverlauf, um verlorene Commits wiederzufinden
|
||||||
|
|
||||||
|
## Stash
|
||||||
|
- `git stash push -m "<text>"` — Änderungen wegpacken
|
||||||
|
- `git stash push -u` — inklusive unversionierter Dateien
|
||||||
|
- `git stash list` — Stashes anzeigen
|
||||||
|
- `git stash pop` — letzten Stash zurückholen und entfernen
|
||||||
|
- `git stash apply <stash>` — anwenden, aber behalten
|
||||||
|
|
||||||
|
## Worktrees
|
||||||
|
- `git worktree list` — alle Worktrees
|
||||||
|
- `git worktree add <pfad> <branch>` — Worktree für bestehenden Branch
|
||||||
|
- `git worktree add -b <name> <pfad>` — Worktree mit neuem Branch
|
||||||
|
- `git worktree remove <pfad>` — Worktree entfernen
|
||||||
|
- `git worktree prune` — verwaiste Einträge aufräumen
|
||||||
|
|
||||||
|
## Suchen
|
||||||
|
- `git grep -n "<text>"` — im Arbeitsbaum suchen
|
||||||
|
- `git log -S"<text>"` — Commits, die den Text einfügen oder entfernen
|
||||||
|
- `git blame -L 40,60 <pfad>` — Zeilenbereich zuordnen
|
||||||
|
- `git bisect start` — binäre Suche nach dem Fehler-Commit
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# PowerShell
|
||||||
|
|
||||||
|
## Navigieren und Dateien
|
||||||
|
- `Get-ChildItem` — Verzeichnis auflisten (`ls`, `-Force` zeigt versteckte)
|
||||||
|
- `Set-Location <pfad>` — Verzeichnis wechseln (`cd`)
|
||||||
|
- `New-Item -ItemType Directory <pfad>` — Ordner anlegen (`mkdir`)
|
||||||
|
- `Copy-Item <quelle> <ziel> -Recurse` — kopieren
|
||||||
|
- `Move-Item <quelle> <ziel>` — verschieben oder umbenennen
|
||||||
|
- `Remove-Item <pfad> -Recurse -Force` — löschen, endgültig
|
||||||
|
- `Test-Path <pfad>` — existiert die Datei oder der Ordner?
|
||||||
|
|
||||||
|
## Ansehen und Suchen
|
||||||
|
- `Get-Content <datei> -Tail 50` — letzte Zeilen (`-Wait` folgt wie tail -f)
|
||||||
|
- `Select-String "<text>" <dateien> -List` — in Dateien suchen (grep)
|
||||||
|
- `Get-ChildItem -Recurse -Filter *.log` — Dateien rekursiv finden
|
||||||
|
- `<befehl> | Select-Object -First 10` — Ausgabe begrenzen (head)
|
||||||
|
- `<befehl> | Where-Object Name -like "*test*"` — Ausgabe filtern
|
||||||
|
- `<befehl> | Measure-Object` — zählen (wc)
|
||||||
|
- `<befehl> | Out-GridView` — Ausgabe in sortierbarem Fenster
|
||||||
|
|
||||||
|
## Prozesse und Dienste
|
||||||
|
- `Get-Process | Sort-Object CPU -Descending | Select-Object -First 10` — Top-Prozesse
|
||||||
|
- `Stop-Process -Name <name>` — Prozess beenden (`-Id <pid>` per PID)
|
||||||
|
- `Get-Service <name>` — Dienststatus
|
||||||
|
- `Restart-Service <name>` — Dienst neu starten (Admin nötig)
|
||||||
|
|
||||||
|
## Netzwerk
|
||||||
|
- `Test-NetConnection <host> -Port 443` — Erreichbarkeit und Port testen
|
||||||
|
- `Get-NetTCPConnection -LocalPort <port>` — wer belegt den Port (OwningProcess = PID)
|
||||||
|
- `Invoke-RestMethod <url>` — HTTP-Aufruf, JSON kommt als Objekt zurück
|
||||||
|
- `Resolve-DnsName <host>` — DNS-Auflösung prüfen
|
||||||
|
|
||||||
|
## Umgebung
|
||||||
|
- `$env:PATH -split ";"` — PATH lesbar ausgeben
|
||||||
|
- `$env:NAME = "wert"` — Variable für diese Session setzen
|
||||||
|
- `[Environment]::SetEnvironmentVariable("NAME", "wert", "User")` — dauerhaft setzen
|
||||||
|
- `Get-Command <name>` — wo liegt ein Befehl (which)
|
||||||
|
- `Get-History` — bisherige Befehle dieser Session
|
||||||
|
|
||||||
|
## Windows-Spezifisches
|
||||||
|
- `explorer .` — aktuellen Ordner im Explorer öffnen
|
||||||
|
- `Start-Process <datei>` — mit Standardprogramm öffnen
|
||||||
|
- `Start-Process pwsh -Verb RunAs` — Terminal als Administrator
|
||||||
|
- `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` — lokale Skripte erlauben
|
||||||
|
- `winget search <name>` — Software im Paketmanager suchen
|
||||||
|
- `winget install <id>` — installieren
|
||||||
|
- `winget upgrade --all` — alles aktualisieren
|
||||||
|
|
||||||
|
## Hilfe
|
||||||
|
- `Get-Help <befehl> -Examples` — Beispiele zu einem Cmdlet
|
||||||
|
- `Get-Member` — Eigenschaften eines Pipeline-Objekts anzeigen
|
||||||
|
- `Get-Alias <kurzform>` — welches Cmdlet steckt hinter einem Alias
|
||||||
@@ -58,13 +58,25 @@ Design/ — Tokens.axaml (design tokens; merged before styles)
|
|||||||
| `ListSettingsModalViewModel` | Name, working dir, commit type, "manual list" flag, `VerifyCommand`, delete. Hosts the shared `AgentConfigEditorViewModel` as `Agent` (scope=List) — ⚠️ save delegates to `Agent.SaveAsync(verifyCommand)` because both land in the same `list_config` row via one `UpdateListConfig` call and would otherwise clobber each other. |
|
| `ListSettingsModalViewModel` | Name, working dir, commit type, "manual list" flag, `VerifyCommand`, delete. Hosts the shared `AgentConfigEditorViewModel` as `Agent` (scope=List) — ⚠️ save delegates to `Agent.SaveAsync(verifyCommand)` because both land in the same `list_config` row via one `UpdateListConfig` call and would otherwise clobber each other. |
|
||||||
| `WeeklyReportModalViewModel` | Range pickers default "since last standup weekday → today", cached per range. |
|
| `WeeklyReportModalViewModel` | Range pickers default "since last standup weekday → today", cached per range. |
|
||||||
| `MergeHelperSelectionModalViewModel` | "Let Claude handle it" picker → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md). |
|
| `MergeHelperSelectionModalViewModel` | "Let Claude handle it" picker → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md). |
|
||||||
| `UsageMonitorModalViewModel` | Opened from the usage pill (shown **before** the data loads via `BeginLoad`); gauges are **dynamic** per `UsageSnapshotDto.Limits` row, and the 5h/7d ones carry three draggable stage markers (soft/hard/gate) via `UsageGaugeBar` + the pure `UsageThresholdDrag`, plus a colour-matched legend with a `NumericUpDown` per stage → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md). The **analytics** part (models/tasks/cost) comes from the external TokenTracker export, not from us: without the CLI the modal shows a hint card with an install button (gated on Node ≥ 20) instead of the totals header. Gauges, gate and throttle are unaffected by it. |
|
| `UsageMonitorModalViewModel` | Opened from the usage pill (shown **before** the data loads via `BeginLoad`); gauges are **dynamic** per `UsageSnapshotDto.Limits` row, and the 5h/7d ones carry three draggable stage markers (soft/hard/gate) via `UsageGaugeBar` + the pure `UsageThresholdDrag`, plus a colour-matched legend with a `NumericUpDown` per stage → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md). The **analytics** part (models/tasks/cost) comes from the external TokenTracker export, not from us: without the CLI the modal shows a hint card with an install button (gated on Node ≥ 20) instead of the totals header. Gauges, gate and throttle are unaffected by it. The modal is **display + threshold drag only** — the PowerUse switch/window/handoff % it used to host moved to Settings → Execution (`GeneralSettingsTabViewModel`), and "Refresh now" is an icon in the titlebar via `ModalShell.HeaderActions` rather than a body row. |
|
||||||
|
|
||||||
Self-explanatory: `RepoImportModalViewModel` (bulk-create lists from git repos; already-wired
|
Self-explanatory: `RepoImportModalViewModel` (bulk-create lists from git repos; already-wired
|
||||||
repos disabled), `MergeModalViewModel`, `WorktreesOverviewModalViewModel`,
|
repos disabled), `MergeModalViewModel`, `WorktreesOverviewModalViewModel`,
|
||||||
`UnfinishedPlanningModalViewModel`, `LogVisualizerViewModel` (last 30 min, all levels + a
|
`UnfinishedPlanningModalViewModel`, `LogVisualizerViewModel` (last 30 min, all levels + a
|
||||||
warn/error filter), `WorkerConnectionModalViewModel`, `AboutModalViewModel`.
|
warn/error filter), `WorkerConnectionModalViewModel`, `AboutModalViewModel`.
|
||||||
|
|
||||||
|
`CheatSheetsViewModel` backs the modeless `CheatSheetsWindow` (Help → Cheat Sheets): content is
|
||||||
|
markdown under `Assets/CheatSheets/*.md` as **EmbeddedResource** (not `AvaloniaResource` —
|
||||||
|
`AssetLoader` throws without an initialized Avalonia app, which would make the VM unconstructible
|
||||||
|
in tests), rendered through `MarkdownView`, with a `Topmost` pin so it can sit docked beside the
|
||||||
|
shell. A new sheet = a new `.md` file + its key in `CheatSheetsViewModel.Keys` (+ a menu entry if
|
||||||
|
it should be directly reachable). `MarkdownView` has no table/fenced-code support, so sheets are
|
||||||
|
bullet lists with inline code — rendered via its opt-in `CommandRows` property, which turns a
|
||||||
|
`` `command` — description `` bullet into a mono chip + copy button + description and wraps each
|
||||||
|
`##` section in a collapsible `Expander` (blocks are added to the open section's panel, not to the
|
||||||
|
host). The flag is opt-in because the other three `MarkdownView` call sites (prompt-file viewer,
|
||||||
|
weekly report, task-description preview) render prose where copy buttons would be noise.
|
||||||
|
|
||||||
## Diff & Conflicts
|
## Diff & Conflicts
|
||||||
|
|
||||||
`UnifiedDiffParser` (static) + `DiffModels.cs` shared types + `DiffViewerViewModel` (one unified
|
`UnifiedDiffParser` (static) + `DiffModels.cs` shared types + `DiffViewerViewModel` (one unified
|
||||||
@@ -106,6 +118,9 @@ There is no null→bool converter — bind
|
|||||||
Modals use `TaskCompletionSource` results behind the reusable `ModalShell` control — the dialog
|
Modals use `TaskCompletionSource` results behind the reusable `ModalShell` control — the dialog
|
||||||
sets the result on save/cancel, the caller awaits the TCS.
|
sets the result on save/cancel, the caller awaits the TCS.
|
||||||
|
|
||||||
|
`ModalShell.HeaderActions` is the titlebar slot left of maximise/close — for a window-level action
|
||||||
|
that should stay visible without costing a body row (currently only the usage monitor's refresh).
|
||||||
|
|
||||||
`ModalShell` also owns the window chrome for every modal: the titlebar drag goes through
|
`ModalShell` also owns the window chrome for every modal: the titlebar drag goes through
|
||||||
`Window.BeginMoveDrag` (so Windows snap works — drag to the top edge to maximise; the OS only
|
`Window.BeginMoveDrag` (so Windows snap works — drag to the top edge to maximise; the OS only
|
||||||
snaps `CanResize="True"` windows, which is the opt-in), and it insets itself by the window's
|
snaps `CanResize="True"` windows, which is the opt-in), and it insets itself by the window's
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<!-- EmbeddedResource, not AvaloniaResource: reading these needs no initialized Avalonia
|
||||||
|
app, so the VM stays constructible in tests. -->
|
||||||
|
<EmbeddedResource Include="Assets/CheatSheets/*.md" />
|
||||||
<AvaloniaResource Include="Assets/Fonts/*.ttf" />
|
<AvaloniaResource Include="Assets/Fonts/*.ttf" />
|
||||||
<AvaloniaResource Include="Assets/Fonts/OFL-InterTight.txt" />
|
<AvaloniaResource Include="Assets/Fonts/OFL-InterTight.txt" />
|
||||||
<AvaloniaResource Include="Assets/Fonts/OFL-JetBrainsMono.txt" />
|
<AvaloniaResource Include="Assets/Fonts/OFL-JetBrainsMono.txt" />
|
||||||
|
|||||||
@@ -36,4 +36,7 @@ public interface IDialogService
|
|||||||
|
|
||||||
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
|
||||||
void ShowMissionControl(MissionControlViewModel vm);
|
void ShowMissionControl(MissionControlViewModel vm);
|
||||||
|
|
||||||
|
/// <summary>Show (or re-show + focus) the modeless cheat-sheet window for the shell's shared VM.</summary>
|
||||||
|
void ShowCheatSheets(CheatSheetsViewModel vm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,6 +515,19 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
|
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One shared VM so the sheets are parsed once and the window keeps its selection/pin
|
||||||
|
// across reopens; the window itself is single-instance in the dialog service.
|
||||||
|
private CheatSheetsViewModel? _cheatSheets;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenCheatSheet(string? key)
|
||||||
|
{
|
||||||
|
if (Dialogs is null) return;
|
||||||
|
_cheatSheets ??= new CheatSheetsViewModel();
|
||||||
|
if (!string.IsNullOrEmpty(key)) _cheatSheets.Select(key);
|
||||||
|
Dialogs.ShowCheatSheets(_cheatSheets);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OpenLogVisualizer()
|
private async Task OpenLogVisualizer()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
/// <summary>One cheat sheet: title from the file's leading <c># </c> heading, body is the rest.</summary>
|
||||||
|
public sealed record CheatSheet(string Key, string Title, string Body);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backs the non-modal cheat-sheet window (Help → Cheat Sheets). Content lives as markdown
|
||||||
|
/// under <c>Assets/CheatSheets/*.md</c> (embedded resources) and is rendered by the existing
|
||||||
|
/// <c>MarkdownView</c>,
|
||||||
|
/// so a new sheet is a new file — but the key list here stays explicit to keep the order
|
||||||
|
/// deterministic and a missing resource visible instead of silently absent.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CheatSheetsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private static readonly string[] Keys =
|
||||||
|
["git", "conventional-commits", "claude-cli", "claude-config", "dotnet-cli", "powershell"];
|
||||||
|
|
||||||
|
public ObservableCollection<CheatSheet> Sheets { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private CheatSheet? _selectedSheet;
|
||||||
|
|
||||||
|
/// <summary>Keeps the window above the shell so it can sit docked at a screen edge.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isPinned;
|
||||||
|
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
public CheatSheetsViewModel()
|
||||||
|
{
|
||||||
|
foreach (var key in Keys) Sheets.Add(Load(key));
|
||||||
|
SelectedSheet = Sheets.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Preselects a sheet by key; unknown keys keep the current selection.</summary>
|
||||||
|
public void Select(string key)
|
||||||
|
{
|
||||||
|
var sheet = Sheets.FirstOrDefault(s => s.Key == key);
|
||||||
|
if (sheet is not null) SelectedSheet = sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Close() => CloseAction?.Invoke();
|
||||||
|
|
||||||
|
private static CheatSheet Load(string key)
|
||||||
|
{
|
||||||
|
var assembly = typeof(CheatSheetsViewModel).Assembly;
|
||||||
|
// Match by suffix rather than composing the logical name from RootNamespace — the
|
||||||
|
// prefix is a build detail, and a rename shouldn't silently blank the sheet.
|
||||||
|
var name = assembly.GetManifestResourceNames()
|
||||||
|
.FirstOrDefault(n => n.EndsWith($".CheatSheets.{key}.md", StringComparison.Ordinal));
|
||||||
|
var stream = name is null ? null : assembly.GetManifestResourceStream(name);
|
||||||
|
if (stream is null)
|
||||||
|
return new CheatSheet(key, key, Loc.T("modals.cheatSheets.missing", $"{key}.md"));
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var text = reader.ReadToEnd().Replace("\r\n", "\n");
|
||||||
|
|
||||||
|
// The window shows the title in its own selector, so strip the leading heading
|
||||||
|
// rather than rendering it a second time inside the body.
|
||||||
|
var lines = new List<string>(text.Split('\n'));
|
||||||
|
var title = key;
|
||||||
|
var first = lines.FindIndex(l => !string.IsNullOrWhiteSpace(l));
|
||||||
|
if (first >= 0 && lines[first].StartsWith("# ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
title = lines[first][2..].Trim();
|
||||||
|
lines.RemoveRange(0, first + 1);
|
||||||
|
}
|
||||||
|
return new CheatSheet(key, title, string.Join('\n', lines).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Documents;
|
using Avalonia.Controls.Documents;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.Controls;
|
namespace ClaudeDo.Ui.Views.Controls;
|
||||||
|
|
||||||
@@ -13,12 +17,27 @@ public partial class MarkdownView : UserControl
|
|||||||
public static readonly StyledProperty<string?> MarkdownProperty =
|
public static readonly StyledProperty<string?> MarkdownProperty =
|
||||||
AvaloniaProperty.Register<MarkdownView, string?>(nameof(Markdown));
|
AvaloniaProperty.Register<MarkdownView, string?>(nameof(Markdown));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a bullet that starts with inline code as a command row: the command as a
|
||||||
|
/// monospace chip with a copy button, the rest as a description below it, and a rule under
|
||||||
|
/// each <c>##</c> section. Opt-in — the prompt-file viewer and the weekly report render
|
||||||
|
/// prose where nearly every bullet carries inline code, and copy buttons there are noise.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly StyledProperty<bool> CommandRowsProperty =
|
||||||
|
AvaloniaProperty.Register<MarkdownView, bool>(nameof(CommandRows));
|
||||||
|
|
||||||
public string? Markdown
|
public string? Markdown
|
||||||
{
|
{
|
||||||
get => GetValue(MarkdownProperty);
|
get => GetValue(MarkdownProperty);
|
||||||
set => SetValue(MarkdownProperty, value);
|
set => SetValue(MarkdownProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CommandRows
|
||||||
|
{
|
||||||
|
get => GetValue(CommandRowsProperty);
|
||||||
|
set => SetValue(CommandRowsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
private StackPanel? _host;
|
private StackPanel? _host;
|
||||||
|
|
||||||
public MarkdownView()
|
public MarkdownView()
|
||||||
@@ -34,6 +53,8 @@ public partial class MarkdownView : UserControl
|
|||||||
base.OnPropertyChanged(change);
|
base.OnPropertyChanged(change);
|
||||||
if (change.Property == MarkdownProperty)
|
if (change.Property == MarkdownProperty)
|
||||||
Render(change.GetNewValue<string?>());
|
Render(change.GetNewValue<string?>());
|
||||||
|
else if (change.Property == CommandRowsProperty)
|
||||||
|
Render(Markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Regex NumberedItem = new(@"^\s*(\d+)\.\s+(.*)$", RegexOptions.Compiled);
|
private static readonly Regex NumberedItem = new(@"^\s*(\d+)\.\s+(.*)$", RegexOptions.Compiled);
|
||||||
@@ -48,6 +69,9 @@ public partial class MarkdownView : UserControl
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var lines = md.Replace("\r\n", "\n").Split('\n');
|
var lines = md.Replace("\r\n", "\n").Split('\n');
|
||||||
|
// In CommandRows mode every "## " opens a collapsible section and blocks land inside it;
|
||||||
|
// otherwise this stays the host for the whole document.
|
||||||
|
var target = _host;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (i < lines.Length)
|
while (i < lines.Length)
|
||||||
{
|
{
|
||||||
@@ -56,19 +80,22 @@ public partial class MarkdownView : UserControl
|
|||||||
|
|
||||||
if (line.StartsWith("### "))
|
if (line.StartsWith("### "))
|
||||||
{
|
{
|
||||||
_host.Children.Add(Heading(line[4..], 13));
|
target.Children.Add(Heading(line[4..], 13));
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.StartsWith("## "))
|
if (line.StartsWith("## "))
|
||||||
{
|
{
|
||||||
_host.Children.Add(Heading(line[3..], 15));
|
if (CommandRows)
|
||||||
|
target = OpenSection(line[3..]);
|
||||||
|
else
|
||||||
|
target.Children.Add(Heading(line[3..], 15));
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.StartsWith("# "))
|
if (line.StartsWith("# "))
|
||||||
{
|
{
|
||||||
_host.Children.Add(Heading(line[2..], 17));
|
target.Children.Add(Heading(line[2..], 17));
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -77,7 +104,10 @@ public partial class MarkdownView : UserControl
|
|||||||
{
|
{
|
||||||
while (i < lines.Length && (lines[i].StartsWith("- ") || lines[i].StartsWith("* ")))
|
while (i < lines.Length && (lines[i].StartsWith("- ") || lines[i].StartsWith("* ")))
|
||||||
{
|
{
|
||||||
_host.Children.Add(Bullet(lines[i][2..]));
|
var item = lines[i][2..];
|
||||||
|
target.Children.Add(CommandRows && TrySplitCommand(item, out var cmd, out var desc)
|
||||||
|
? CommandRow(cmd, desc)
|
||||||
|
: Bullet(item));
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -89,7 +119,7 @@ public partial class MarkdownView : UserControl
|
|||||||
while (i < lines.Length && NumberedItem.IsMatch(lines[i]))
|
while (i < lines.Length && NumberedItem.IsMatch(lines[i]))
|
||||||
{
|
{
|
||||||
var m = NumberedItem.Match(lines[i]);
|
var m = NumberedItem.Match(lines[i]);
|
||||||
_host.Children.Add(Numbered(m.Groups[1].Value, m.Groups[2].Value));
|
target.Children.Add(Numbered(m.Groups[1].Value, m.Groups[2].Value));
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -104,7 +134,7 @@ public partial class MarkdownView : UserControl
|
|||||||
paragraph.Append(lines[i].Trim());
|
paragraph.Append(lines[i].Trim());
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
_host.Children.Add(Paragraph(paragraph.ToString()));
|
target.Children.Add(Paragraph(paragraph.ToString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +156,120 @@ public partial class MarkdownView : UserControl
|
|||||||
return tb;
|
return tb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a collapsible section for a <c>##</c> heading and returns the panel its blocks go
|
||||||
|
/// into. Native <see cref="Expander"/> — it carries the chevron, keyboard toggle and
|
||||||
|
/// animation, and the app already uses it unstyled in the worktrees overview.
|
||||||
|
/// </summary>
|
||||||
|
private StackPanel OpenSection(string title)
|
||||||
|
{
|
||||||
|
var header = new TextBlock
|
||||||
|
{
|
||||||
|
FontSize = 15,
|
||||||
|
FontWeight = FontWeight.SemiBold,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
};
|
||||||
|
AppendInlines(header.Inlines!, title);
|
||||||
|
|
||||||
|
var body = new StackPanel { Spacing = 6 };
|
||||||
|
_host!.Children.Add(new Expander
|
||||||
|
{
|
||||||
|
Header = header,
|
||||||
|
Content = body,
|
||||||
|
IsExpanded = true,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||||
|
});
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits <c>`command` — description</c> into its two parts. False when the bullet doesn't
|
||||||
|
/// open with inline code, so prose bullets keep the plain renderer.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool TrySplitCommand(string text, out string command, out string description)
|
||||||
|
{
|
||||||
|
command = "";
|
||||||
|
description = "";
|
||||||
|
if (text.Length < 3 || text[0] != '`') return false;
|
||||||
|
var close = text.IndexOf('`', 1);
|
||||||
|
if (close <= 1) return false;
|
||||||
|
command = text[1..close];
|
||||||
|
description = text[(close + 1)..].TrimStart(' ', '—', '–', '-', ':').Trim();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control CommandRow(string command, string description)
|
||||||
|
{
|
||||||
|
var grid = new Grid
|
||||||
|
{
|
||||||
|
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
|
||||||
|
Margin = new Thickness(0, 2, 0, 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
var text = new SelectableTextBlock { Text = command, TextWrapping = TextWrapping.Wrap };
|
||||||
|
// Bound, not resolved: Render can run while detached, where a resource lookup finds
|
||||||
|
// nothing and would freeze on the fallback.
|
||||||
|
text.Bind(TextBlock.FontFamilyProperty, this.GetResourceObservable("MonoFont"));
|
||||||
|
text.Bind(TextBlock.FontSizeProperty, this.GetResourceObservable("FontSizeMono"));
|
||||||
|
|
||||||
|
var chip = new Border
|
||||||
|
{
|
||||||
|
CornerRadius = new CornerRadius(4),
|
||||||
|
Padding = new Thickness(7, 4),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
Child = text,
|
||||||
|
};
|
||||||
|
chip.Bind(Border.BackgroundProperty, this.GetResourceObservable("Surface2Brush"));
|
||||||
|
|
||||||
|
var stack = new StackPanel { Spacing = 3, Children = { chip } };
|
||||||
|
if (description.Length > 0)
|
||||||
|
{
|
||||||
|
var desc = new TextBlock
|
||||||
|
{
|
||||||
|
Classes = { "body" },
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Margin = new Thickness(2, 0, 0, 0),
|
||||||
|
};
|
||||||
|
AppendInlines(desc.Inlines!, description);
|
||||||
|
stack.Children.Add(desc);
|
||||||
|
}
|
||||||
|
Grid.SetColumn(stack, 0);
|
||||||
|
grid.Children.Add(stack);
|
||||||
|
|
||||||
|
var copy = CopyButton(command);
|
||||||
|
Grid.SetColumn(copy, 1);
|
||||||
|
grid.Children.Add(copy);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button CopyButton(string command)
|
||||||
|
{
|
||||||
|
var copyIcon = new PathIcon { Width = 11, Height = 11 };
|
||||||
|
copyIcon.Bind(PathIcon.DataProperty, this.GetResourceObservable("Icon.Copy"));
|
||||||
|
var checkIcon = new PathIcon { Width = 11, Height = 11 };
|
||||||
|
checkIcon.Bind(PathIcon.DataProperty, this.GetResourceObservable("Icon.Check"));
|
||||||
|
|
||||||
|
var button = new Button
|
||||||
|
{
|
||||||
|
Classes = { "icon-btn" },
|
||||||
|
Content = copyIcon,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(6, 0, 0, 0),
|
||||||
|
};
|
||||||
|
ToolTip.SetTip(button, Loc.T("controls.markdown.copy"));
|
||||||
|
|
||||||
|
button.Click += async (_, _) =>
|
||||||
|
{
|
||||||
|
if (TopLevel.GetTopLevel(this)?.Clipboard is not { } clipboard) return;
|
||||||
|
await clipboard.SetTextAsync(command);
|
||||||
|
// The clipboard gives no feedback of its own; a brief check mark is the confirmation.
|
||||||
|
button.Content = checkIcon;
|
||||||
|
DispatcherTimer.RunOnce(() => button.Content = copyIcon, TimeSpan.FromSeconds(1.5));
|
||||||
|
};
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
private static Control Paragraph(string text)
|
private static Control Paragraph(string text)
|
||||||
{
|
{
|
||||||
var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
|
var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
|
||||||
|
|||||||
@@ -80,6 +80,20 @@
|
|||||||
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
||||||
Command="{Binding CheckForUpdatesCommand}"/>
|
Command="{Binding CheckForUpdatesCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
|
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheets}">
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetGit}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="git"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetCommits}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="conventional-commits"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetClaude}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="claude-cli"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetClaudeConfig}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="claude-config"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetDotnet}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="dotnet-cli"/>
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.cheatSheetPowershell}"
|
||||||
|
Command="{Binding OpenCheatSheetCommand}" CommandParameter="powershell"/>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
|
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<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.CheatSheetsWindow"
|
||||||
|
x:DataType="vm:CheatSheetsViewModel"
|
||||||
|
Title="{loc:Tr modals.cheatSheets.windowTitle}"
|
||||||
|
Width="480" Height="820" MinWidth="320" MinHeight="300"
|
||||||
|
CanResize="True"
|
||||||
|
Topmost="{Binding IsPinned}"
|
||||||
|
WindowDecorations="BorderOnly"
|
||||||
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
|
ExtendClientAreaTitleBarHeightHint="-1"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{DynamicResource SurfaceBrush}">
|
||||||
|
|
||||||
|
<Window.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
||||||
|
</Window.KeyBindings>
|
||||||
|
|
||||||
|
<ctl:ModalShell Title="{loc:Tr modals.cheatSheets.title}" CloseCommand="{Binding CloseCommand}">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Sheet picker + pin. Pin keeps the window above the shell while it sits at a screen edge. -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,10,16,0">
|
||||||
|
<ComboBox ItemsSource="{Binding Sheets}"
|
||||||
|
SelectedItem="{Binding SelectedSheet}"
|
||||||
|
DisplayMemberBinding="{Binding Title}"
|
||||||
|
MinWidth="180"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<ToggleButton Content="{loc:Tr modals.cheatSheets.pin}"
|
||||||
|
IsChecked="{Binding IsPinned}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="{loc:Tr modals.cheatSheets.pinTooltip}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<!-- Inset on the content, not the ScrollViewer: Avalonia 12 leaves ScrollViewer.Padding
|
||||||
|
out of the Extent, which clips the last lines at max scroll. -->
|
||||||
|
<ctl:MarkdownView Markdown="{Binding SelectedSheet.Body}" CommandRows="True"
|
||||||
|
Margin="16,12,16,16"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</ctl:ModalShell>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
|
public partial class CheatSheetsWindow : Window
|
||||||
|
{
|
||||||
|
public CheatSheetsWindow() => InitializeComponent();
|
||||||
|
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
private readonly Window _owner;
|
private readonly Window _owner;
|
||||||
private MissionControlWindow? _missionControl;
|
private MissionControlWindow? _missionControl;
|
||||||
private DiffViewerView? _diffViewerWindow;
|
private DiffViewerView? _diffViewerWindow;
|
||||||
|
private CheatSheetsWindow? _cheatSheets;
|
||||||
|
|
||||||
public WindowDialogService(Window owner) => _owner = owner;
|
public WindowDialogService(Window owner) => _owner = owner;
|
||||||
|
|
||||||
@@ -182,9 +183,29 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
_missionControl ??= new MissionControlWindow { DataContext = vm };
|
_missionControl ??= new MissionControlWindow { DataContext = vm };
|
||||||
if (!_missionControl.IsVisible)
|
if (!_missionControl.IsVisible)
|
||||||
_missionControl.Show(); // modeless, independent top-level window
|
_missionControl.Show(); // modeless, independent top-level window
|
||||||
|
// Activate() alone does not restore a minimised window — it stays in the taskbar and the
|
||||||
|
// button looks dead. Un-minimise first, then take focus.
|
||||||
|
if (_missionControl.WindowState == WindowState.Minimized)
|
||||||
|
_missionControl.WindowState = WindowState.Normal;
|
||||||
_missionControl.Activate(); // bring to front / focus
|
_missionControl.Activate(); // bring to front / focus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowCheatSheets(CheatSheetsViewModel vm)
|
||||||
|
{
|
||||||
|
// Modeless and single-instance, like Mission Control: the point is to park it beside the
|
||||||
|
// shell, so a second window (or a modal that blocks the shell) would defeat it.
|
||||||
|
if (_cheatSheets is null)
|
||||||
|
{
|
||||||
|
_cheatSheets = new CheatSheetsWindow { DataContext = vm };
|
||||||
|
vm.CloseAction = () => _cheatSheets?.Close();
|
||||||
|
_cheatSheets.Closed += (_, _) => _cheatSheets = null;
|
||||||
|
_cheatSheets.Show(_owner);
|
||||||
|
}
|
||||||
|
if (_cheatSheets.WindowState == WindowState.Minimized)
|
||||||
|
_cheatSheets.WindowState = WindowState.Normal;
|
||||||
|
_cheatSheets.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
public Task<bool> ConfirmAsync(string message)
|
public Task<bool> ConfirmAsync(string message)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||||
|
|
||||||
|
public class CheatSheetsViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LoadsEverySheetWithTitleFromHeading()
|
||||||
|
{
|
||||||
|
var vm = new CheatSheetsViewModel();
|
||||||
|
|
||||||
|
Assert.Equal(6, vm.Sheets.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
["Git", "Conventional Commits", "Claude CLI", "Claude Konfiguration", "dotnet CLI", "PowerShell"],
|
||||||
|
vm.Sheets.Select(s => s.Title));
|
||||||
|
// The heading is stripped so the window doesn't render the title twice.
|
||||||
|
Assert.All(vm.Sheets, s => Assert.StartsWith("## ", s.Body));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSwitchesSheetAndIgnoresUnknownKeys()
|
||||||
|
{
|
||||||
|
var vm = new CheatSheetsViewModel();
|
||||||
|
Assert.Equal("git", vm.SelectedSheet!.Key);
|
||||||
|
|
||||||
|
vm.Select("dotnet-cli");
|
||||||
|
Assert.Equal("dotnet-cli", vm.SelectedSheet!.Key);
|
||||||
|
|
||||||
|
vm.Select("nope");
|
||||||
|
Assert.Equal("dotnet-cli", vm.SelectedSheet!.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ public class ListsIslandCreateListCancelTests : IDisposable
|
|||||||
public Task<bool> ConfirmAsync(string message) => Task.FromResult(true);
|
public Task<bool> ConfirmAsync(string message) => Task.FromResult(true);
|
||||||
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
||||||
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
||||||
|
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ListSettingsProvider(Func<ListSettingsModalViewModel> create) : IServiceProvider
|
private sealed class ListSettingsProvider(Func<ListSettingsModalViewModel> create) : IServiceProvider
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public class TasksIslandMoveToListTests : IDisposable
|
|||||||
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
|
public Task ShowLogVisualizerAsync(LogVisualizerViewModel vm) => throw new NotImplementedException();
|
||||||
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
public Task ShowErrorAsync(string message) => Task.CompletedTask;
|
||||||
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
public void ShowMissionControl(MissionControlViewModel vm) => throw new NotImplementedException();
|
||||||
|
public void ShowCheatSheets(CheatSheetsViewModel vm) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TasksIslandViewModel BuildViewModel() => new(new TestDbFactory(NewContext), worker: null);
|
private TasksIslandViewModel BuildViewModel() => new(new TestDbFactory(NewContext), worker: null);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ClaudeDo.Ui.Views.Controls;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Covers the command-row split only — rendering itself needs an Avalonia app, which this
|
||||||
|
/// project has no host for.
|
||||||
|
/// </summary>
|
||||||
|
public class MarkdownCommandRowTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("`git status -sb` — Kurzstatus mit Branch-Zeile", "git status -sb", "Kurzstatus mit Branch-Zeile")]
|
||||||
|
[InlineData("`git branch -d <name>` — löschen (`-D` erzwingt)", "git branch -d <name>", "löschen (`-D` erzwingt)")]
|
||||||
|
[InlineData("`dotnet build`", "dotnet build", "")]
|
||||||
|
public void SplitsCommandFromDescription(string line, string cmd, string desc)
|
||||||
|
{
|
||||||
|
Assert.True(MarkdownView.TrySplitCommand(line, out var actualCmd, out var actualDesc));
|
||||||
|
Assert.Equal(cmd, actualCmd);
|
||||||
|
Assert.Equal(desc, actualDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Prosa ohne Befehl")]
|
||||||
|
[InlineData("Text mit `code` in der Mitte")]
|
||||||
|
[InlineData("`unclosed backtick")]
|
||||||
|
[InlineData("``")]
|
||||||
|
public void LeavesNonCommandBulletsToThePlainRenderer(string line)
|
||||||
|
{
|
||||||
|
Assert.False(MarkdownView.TrySplitCommand(line, out _, out _));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user