# Offene Punkte — bei jedem Start prüfen und abhaken

jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:"SessionStart", additionalContext: .}}' /home/marcuh/claude-projects/ai-control/OFFENE-PUNKTE.md


# Offene Punkte — bei jedem Start prüfen und abhaken


kannst du den Pool privateDefault in private umbennen?


CronCreate CronDelete CronList DesignSync EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Monitor NotebookEdit PushNotification RemoteTrigger SendMessage TaskCreate TaskGet TaskList TaskOutput TaskStop TaskUpdate WebFetch WebSearch


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- git-sync: Commit + Push aller persistent konfigurierten Projekt-Repos
- deep-research: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. - When the user wants a deep, multi-source, fact-checked research report on any topic. BEFORE invoking, check if the question is specific enough to research directly — if underspecified (e.g., "what car to buy" without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.
- dataviz: Use this skill whenever you are about to create ANY chart, graph, plot, dashboard, or data visualization, in ANY output medium — an HTML or React artifact, inline SVG, plotting code in any library (matplotlib, plotly, d3, Recharts, …), an image/PNG you will render and upload, or a chart shared into Slack. Read it BEFORE writing the first line of chart code, choosing chart colors, building a stat tile / meter / KPI row, or laying out a dashboard. Produces visualizations that read as one system — elegant, accessible, consistent in light and dark — using a brand-neutral placeholder palette you swap for your own. Teaches a design-system-agnostic method: a form heuristic, a color formula with a runnable validator, mark specs, and interaction rules. A validated default palette is documented in `references/palette.md` — swap that file's values for your brand's. Triggers on: "chart", "graph", "plot", "data viz", "visualization", "dashboard", "analytics", "visualize data", "categorical colors", "sequential / diverging palette", "stat tile", "sparkline", "heatmap", "legend", "axis", "tooltip", "chart colors", "color by series".
- artifact-design: Design guidance and fundamentals for Artifacts.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by exercising it end-to-end and observing behavior — drive the affected flow, not just tests or typecheck. Run before committing nontrivial changes; bootstraps this repo's project verify skill if none exists yet. Don't invoke it on a diff that only touches tests, docs, or other code with no runtime surface to drive (a change to product source always has one) — there's nothing to observe.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a GitHub pull request; for your working diff use /code-review
- security-review: Complete a security review of the pending changes on the current branch

git-sync deep-research dataviz artifact-design update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Benenne Pool privateDefault in private um


Ich lese kurz die beiden Memory-Dateien zum Pool-Setup, um den Plan korrekt zu machen.


1	---
2	name: ai-control-pool-setup
3	description: "Stand des Pool-Setups seit 2026-07-03 — Layout, Starter, was gelöscht wurde, offene Punkte"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: e108ad75-f176-43b6-901d-c3ec19ec3961
8	---
9
10	Seit 2026-07-03 produktiv:
11
12	- Pools: `~/.config/ai-control/pools/<pool>/` = CLAUDE_CONFIG_DIR. `pool.json` (name, credentialType), oauth: `.credentials.json`, apikey: `apikey` + `apiKeyHelper` in pool-`settings.json`. Pools (umbenannt 2026-07-03 abends): privateDefault (vorher Mx9Privat; oauth), apiDefault (vorher TestWürth; apikey — 2026-07-03 kurz auf oauth, am selben Tag zurück auf apikey via `apiKeyHelper` in Pool-settings.json; model claude-sonnet-5). Alle 7 Projekte → privateDefault; apiDefault derzeit ohne Projekt. Pool-Wechsel eines Projekts = nur noch `ai-control.json` ändern (einzige Quelle; zieht beim nächsten Start). Pool-Umbenennung ändert den Pfad und damit den Keychain-Suffix — alte Einträge greifen dann nicht mehr.
13	- `CLAUDE.md`, `commands/`, `settings.json` liegen als physische Kopien in jedem Pool — bewusst poolspezifisch, keine Symlinks.
14	- Zuordnung: `~/claude-projects/<projekt>/ai-control.json` `{ "pool": ... }`.
15	- Starter: `~/Applications/Ghostty-<projekt>.app`, `LSEnvironment` setzt nur noch XDG_CONFIG_HOME (stabil, nie poolabhängig). Alle Ghostty-Configs starten `command = ~/.local/bin/claude-sync` (ggf. mit `--add-dir`-Args). `claude-sync` löst seit 2026-07-04 den Pool selbst auf (liest `./ai-control.json` im CWD, exportiert `CLAUDE_CONFIG_DIR`, laut scheiternd ohne Pool), macht git pull davor und commit+push danach. `claude-start` ist gelöscht — in `claude-sync` integriert. Env/plist tragen keinen Pool mehr.
16	- Pool-Guard (seit 2026-07-04): `claude-sync` schreibt Sentinel `.ai-control-running` in den Projekt-Root (Inhalt pool=CLAUDE_CONFIG_DIR, pid; FD offen gehalten, nach claude-Ende gelöscht, in .gitignore). Vorab-Check (Endstand 2026-07-03 spät): Fensterzahl statt Prozesszustand, ohne Apple-Events — `ghostty-wincount <app-pid>` (Swift-Helfer, CGWindowListCopyWindowInfo onScreenOnly + Layer 0; Binary `~/.local/bin/ghostty-wincount`, Quelle `~/claude-projects/robotunits/bin/ghostty-wincount.swift`; App-pid via ps+grep auf den App-Pfad). Das Skript läuft selbst in Fenster 1; ≥2 → Arbeitsfenster existiert → `open -a` (Launch Services, kein Prompt) + 4 s + Exit 0; sonst → Sentinel-pid und FD-Holder kill -9, Sentinel kommentarlos löschen, normal starten. Gescheiterte Vorstufen: tty-Heuristik (Zombie behält totes tty), Schreiber-PID-Check (claude-sync-Bash überlebt Ghosttys X-Close manchmal kopflos), osascript count/activate (Automation-Prompt „App X will App Y steuern" = NOGO). Bekannte Grenze: minimierte/Space-fremde Fenster zählen als unsichtbar. Traps in claude-sync: EXIT löscht Sentinel, HUP/TERM killt claude (claude läuft als Hintergrund-Kind mit `<&0` — sonst stdin=/dev/null! — und `wait`), INT no-op; greifen aber bei Ghostty-X/Quit oft nicht (hartes Kill), Rest-Aufräumen beim nächsten Start ist der Normalfall. Alle 7 Ghostty-Configs (`~/.config-ghostty/<projekt>/ghostty/config`): `wait-after-command = false`; claude-sync zeigt Schlussmeldungen per sleep (2 s aktivieren, 3 s Ende, 5 s Fehler), dann schließt das Fenster selbst. SessionStart-Hook `~/claude-projects/robotunits/bin/pool-guard` in allen 7 Projekt-settings.json: Sentinel fehlt / Pool-Mismatch / Schreiber-PID tot → rot blinkender Banner auf /dev/tty + kill des claude-Elternprozesses. RAW-Nutzung außerhalb der Projekte bleibt frei; RAW-Start IM Projekt wird beendet. Achtung: greift auch bei /clear in Sessions, die noch ohne Sentinel gestartet wurden.
17	- Gelöscht: `~/.claude` (komplett), Keychain-Eintrag `Claude Code-credentials`, `~/claude-pools`. Unterhaltungen (90 MB) + `history.jsonl` nach Mx9Privat umgezogen — `/resume` pro Pool.
18	- App-Code (`~/projects/ai-control`, Tauri): Kernlogik mit injizierbarem Home, 22 Tests (`./build.sh`-Toolchain: CARGO_HOME=~/tools/.cargo; node via fnm, nur in interaktiver zsh). Commands unassign_pool/create_project/delete_project, seit 2026-07-04 restart_project (osascript-Quit → warten → open) + Modal in ProjectList: nach Umhängen eines laufenden Projekts Restart-Angebot inkl. Hinweis, dass der claude-sync-Push beim Abschießen entfällt.
19	- Eingebautes Terminal (2026-07-04, Prototyp, funktioniert laut Marcus): `src-tauri/src/terminal.rs` (portable-pty, Commands open_terminal/term_start/term_write/term_resize) + `terminal.html`/`src/terminal.ts` (xterm.js, Output base64-Events, Capability `term-*`). „Terminal"-Button in ProjectList öffnet pro Klick ein eigenes Fenster; PTY startet `zsh -ic claude-sync` im Projektordner — Pool/Sentinel/git-Sync unverändert bei claude-sync, App setzt kein CLAUDE_CONFIG_DIR. Fenster-X killt Kind + schließt PTY (HUP wie Ghostty-X, Abschluss-Push entfällt). Damit Weg frei, die Ghostty-Starter abzulösen ("wir haben alles in der Hand"). Offen: Scrollback-Restore, Fensterposition merken.
20	- Stand 2026-07-04 abends, alles committed+gepusht (bis inkl. Todo-Feature): UI komplett auf Catppuccin-Mocha-Token + JetBrains Mono (frontend-design-Skill; Plugin `frontend-design` war kaputt — installPaths in installed_plugins.json zeigten nach Pool-Umbenennung noch auf Mx9Privat, gefixt), i18n de/en (vue-i18n, Umschalter im Header). Pools: löschbar mit Zuordnungs-Auflösung, „Neu anmelden" löscht vorher den suffixierten Keychain-Eintrag (nur bei ungenutztem Pool, Warn-Dialog), Renew entfernt (Datei-Refresh-Token tot, Keychain hat Vorrang), hasCredentials (apikey-Dateicheck) mit Start-Blockierung. Verbrauch-Tab: usage_stats parst <pool>/projects/*/*.jsonl (Dedup message.id+requestId, Preistabelle Fable 10/50 Opus 5/25 Sonnet 3/15 Haiku 1/5, Cache 1.25x/0.1x), Pool-Zeilen aggregiert + Projekte aufklappbar; apiDefault hat keinerlei Transcripts (Marcus' apiKey-Abfragen vom 04.07. liefen nachweislich nicht über claude-code auf diesem Rechner — Console zeigt die Wahrheit). Projekt-Wizard (create_project_full: Scaffold nach 08-07-Muster inkl. pool-guard-Hook, Arbeitsordner anlegen/verknüpfen) + Löschen (Guard bei laufender Session, optional Arbeitsordner aus additionalDirectories mit). Todo-Feature nur Projektebene (robotunits-Muster: OFFENE-PUNKTE.md + jq-SessionStart-Hook, zuschaltbar in Wizard+Zahnrad; Pool-Ebene bewusst nicht). App heißt ai-control (Cargo-Paket umbenannt), Hand-Icon (Tray per include_bytes + Template-Icon; Dock-Icon der Terminals via NSApplication nur in RunEvent::Ready). 38 Rust-Tests. Offen/geplant: Baustein-Bibliothek (CLAUDE.md-Marker-Mechanismus, Konzept steht: ~/.config/ai-control/blocks/, verwaltete Abschnitte zwischen ai-control:block-Markern), weitere Wizard-Optionen, Split privates Repo / offizielles GitHub-Repo (Ist-Stand als Initial-Commit ohne History; OAuth-Login-Teil wegen ToS-Grauzone fürs öffentliche Repo rausnehmen — apiKey-Teil unkritisch), Vitest für UI-Tests (Vorschlag steht), Dock-Name macOS/Wayland „später überlegen".
21	- Alter Zwischenstand (Commit f747379 = Terminal + Start/Beenden-Button): (a) ProjectList: ein Button pro Zeile — grün „Starten" (open_terminal) / rot „Beenden" (neues Command stop_project: SIGTERM auf exakte PIDs per `pgrep -f -- "--terminal <projekt>$"`, sonst Ghostty-Quit; NIE Quit über die App-Bundle-ID — träfe die Haupt-App); is_running = Ghostty ODER --terminal-Prozess; Status-Kreis ohne Prozess unsichtbar. (b) Terminal-Config-Dialog (Zahnrad): `terminal: { theme, icon, title }` in projekt-`ai-control.json` (pool-Feld jetzt Option, Datei bleibt bei unassign wenn terminal gesetzt); 5 Themes (mocha/dracula/solarized-dark/gruvbox/one-dark, Paletten in terminal.ts, Fenster-BG gespiegelt in terminal.rs theme_background — muss synchron bleiben); Icon = PNG/ICNS-Pfad via tauri-plugin-dialog-Picker, gesetzt per NSApplication.applicationIconImage (objc2, nur in RunEvent::Ready — in setup() überschreibt macOS es wieder); Titel: Fenstertitel + Header, leer → Projektname. `tauri::generate_context!` darf nur 1× expandieren (_EMBED_INFO_PLIST, fällt nur im Release-Build auf).
22	- Dock-Grenze macOS: Hover-Label im Dock = Bundle-Name, zur Laufzeit nicht öffentlich änderbar; Wrapper-Bundle pro Projekt von Marcus abgelehnt („verlieren zu viel"). Windows/X11: Titel+Icon greifen fensterbasiert; Wayland-Docks bräuchten .desktop pro Projekt (xdg-toplevel-icon nur KDE). → „da müssen wir was überlegen, nicht jetzt".
23
24	Frische Pools zeigen Promo-/Upsell-Banner („Werbung"), privateDefault nicht — Ursache ungeklärt (Stand 2026-07-04, nicht weiter vertiefen): Hochsetzen der Zähler in der Pool-`.claude.json` (`tipsHistory.fable-5-promo-2`, `passesUpsellSeenCount`) half nicht, ebenso wenig `cachedGrowthBookFeatures.tengu_birch_lantern = "off"` (einziger Flag-Unterschied der Pools). Kein Settings-Switch vorhanden.
25
26	**Agenda 2026-07-05:** Sync-Test auf neuem Mac (pool-Repo mit Symlink-Runtime: clonen, `link-pool-runtime` bzw. Pool-Anlage über die App, /resume-Übernahme prüfen).
27
28	**Why:** Getrennte Zugänge pro Pool, [[multi-account-keychain]].
29
30	**Agenda 2026-07-04:** Start/Stopp-Architektur grundsätzlich besprechen — Stand vom 2026-07-03 spät (wincount/kill-9/Sentinel-Reste) funktioniert im Test, ist aber Flickwerk um Ghosttys unzuverlässiges Kill-Verhalten herum; Marcus will eine saubere Lösung statt Workarounds. Außerdem: Pool-Guard + Kaltstart über Starter testen; App: UI aufräumen, Projekte managen, Defaults/Standardverhalten (z. B. Todolist); Linux gesondert; Open-Source-Frage („works for my workflow") entschieden nach Feinschliff; außerdem Ghostty und Zed besprechen. Stand App: beta-0.8 gepusht (Tray statt Dock, Autostart-Schalter, Fenster startet unsichtbar, Restart-Dialog).
31
32	**How to apply — offen (Stand 2026-07-03):** (1) Sync-Konzept für `~/.config/ai-control` inkl. Unterhaltungen, Secrets-Frage (git). (2) UI kennt die drei neuen Commands nicht. (3) Erledigt durch Integration in `claude-sync` (2026-07-04): der „Flicker" betraf das gelöschte `claude-start`; `claude-sync` läuft in den Ghostty-Wrappern nachweislich interaktiv.
33

---
name: ai-control-pool-setup
description: "Stand des Pool-Setups seit 2026-07-03 — Layout, Starter, was gelöscht wurde, offene Punkte"
metadata:
  node_type: memory
  type: project
  originSessionId: e108ad75-f176-43b6-901d-c3ec19ec3961
---

Seit 2026-07-03 produktiv:

- Pools: `~/.config/ai-control/pools/<pool>/` = CLAUDE_CONFIG_DIR. `pool.json` (name, credentialType), oauth: `.credentials.json`, apikey: `apikey` + `apiKeyHelper` in pool-`settings.json`. Pools (umbenannt 2026-07-03 abends): privateDefault (vorher Mx9Privat; oauth), apiDefault (vorher TestWürth; apikey — 2026-07-03 kurz auf oauth, am selben Tag zurück auf apikey via `apiKeyHelper` in Pool-settings.json; model claude-sonnet-5). Alle 7 Projekte → privateDefault; apiDefault derzeit ohne Projekt. Pool-Wechsel eines Projekts = nur noch `ai-control.json` ändern (einzige Quelle; zieht beim nächsten Start). Pool-Umbenennung ändert den Pfad und damit den Keychain-Suffix — alte Einträge greifen dann nicht mehr.
- `CLAUDE.md`, `commands/`, `settings.json` liegen als physische Kopien in jedem Pool — bewusst poolspezifisch, keine Symlinks.
- Zuordnung: `~/claude-projects/<projekt>/ai-control.json` `{ "pool": ... }`.
- Starter: `~/Applications/Ghostty-<projekt>.app`, `LSEnvironment` setzt nur noch XDG_CONFIG_HOME (stabil, nie poolabhängig). Alle Ghostty-Configs starten `command = ~/.local/bin/claude-sync` (ggf. mit `--add-dir`-Args). `claude-sync` löst seit 2026-07-04 den Pool selbst auf (liest `./ai-control.json` im CWD, exportiert `CLAUDE_CONFIG_DIR`, laut scheiternd ohne Pool), macht git pull davor und commit+push danach. `claude-start` ist gelöscht — in `claude-sync` integriert. Env/plist tragen keinen Pool mehr.
- Pool-Guard (seit 2026-07-04): `claude-sync` schreibt Sentinel `.ai-control-running` in den Projekt-Root (Inhalt pool=CLAUDE_CONFIG_DIR, pid; FD offen gehalten, nach claude-Ende gelöscht, in .gitignore). Vorab-Check (Endstand 2026-07-03 spät): Fensterzahl statt Prozesszustand, ohne Apple-Events — `ghostty-wincount <app-pid>` (Swift-Helfer, CGWindowListCopyWindowInfo onScreenOnly + Layer 0; Binary `~/.local/bin/ghostty-wincount`, Quelle `~/claude-projects/robotunits/bin/ghostty-wincount.swift`; App-pid via ps+grep auf den App-Pfad). Das Skript läuft selbst in Fenster 1; ≥2 → Arbeitsfenster existiert → `open -a` (Launch Services, kein Prompt) + 4 s + Exit 0; sonst → Sentinel-pid und FD-Holder kill -9, Sentinel kommentarlos löschen, normal starten. Gescheiterte Vorstufen: tty-Heuristik (Zombie behält totes tty), Schreiber-PID-Check (claude-sync-Bash überlebt Ghosttys X-Close manchmal kopflos), osascript count/activate (Automation-Prompt „App X will App Y steuern" = NOGO). Bekannte Grenze: minimierte/Space-fremde Fenster zählen als unsichtbar. Traps in claude-sync: EXIT löscht Sentinel, HUP/TERM killt claude (claude läuft als Hintergrund-Kind mit `<&0` — sonst stdin=/dev/null! — und `wait`), INT no-op; greifen aber bei Ghostty-X/Quit oft nicht (hartes Kill), Rest-Aufräumen beim nächsten Start ist der Normalfall. Alle 7 Ghostty-Configs (`~/.config-ghostty/<projekt>/ghostty/config`): `wait-after-command = false`; claude-sync zeigt Schlussmeldungen per sleep (2 s aktivieren, 3 s Ende, 5 s Fehler), dann schließt das Fenster selbst. SessionStart-Hook `~/claude-projects/robotunits/bin/pool-guard` in allen 7 Projekt-settings.json: Sentinel fehlt / Pool-Mismatch / Schreiber-PID tot → rot blinkender Banner auf /dev/tty + kill des claude-Elternprozesses. RAW-Nutzung außerhalb der Projekte bleibt frei; RAW-Start IM Projekt wird beendet. Achtung: greift auch bei /clear in Sessions, die noch ohne Sentinel gestartet wurden.
- Gelöscht: `~/.claude` (komplett), Keychain-Eintrag `Claude Code-credentials`, `~/claude-pools`. Unterhaltungen (90 MB) + `history.jsonl` nach Mx9Privat umgezogen — `/resume` pro Pool.
- App-Code (`~/projects/ai-control`, Tauri): Kernlogik mit injizierbarem Home, 22 Tests (`./build.sh`-Toolchain: CARGO_HOME=~/tools/.cargo; node via fnm, nur in interaktiver zsh). Commands unassign_pool/create_project/delete_project, seit 2026-07-04 restart_project (osascript-Quit → warten → open) + Modal in ProjectList: nach Umhängen eines laufenden Projekts Restart-Angebot inkl. Hinweis, dass der claude-sync-Push beim Abschießen entfällt.
- Eingebautes Terminal (2026-07-04, Prototyp, funktioniert laut Marcus): `src-tauri/src/terminal.rs` (portable-pty, Commands open_terminal/term_start/term_write/term_resize) + `terminal.html`/`src/terminal.ts` (xterm.js, Output base64-Events, Capability `term-*`). „Terminal"-Button in ProjectList öffnet pro Klick ein eigenes Fenster; PTY startet `zsh -ic claude-sync` im Projektordner — Pool/Sentinel/git-Sync unverändert bei claude-sync, App setzt kein CLAUDE_CONFIG_DIR. Fenster-X killt Kind + schließt PTY (HUP wie Ghostty-X, Abschluss-Push entfällt). Damit Weg frei, die Ghostty-Starter abzulösen ("wir haben alles in der Hand"). Offen: Scrollback-Restore, Fensterposition merken.
- Stand 2026-07-04 abends, alles committed+gepusht (bis inkl. Todo-Feature): UI komplett auf Catppuccin-Mocha-Token + JetBrains Mono (frontend-design-Skill; Plugin `frontend-design` war kaputt — installPaths in installed_plugins.json zeigten nach Pool-Umbenennung noch auf Mx9Privat, gefixt), i18n de/en (vue-i18n, Umschalter im Header). Pools: löschbar mit Zuordnungs-Auflösung, „Neu anmelden" löscht vorher den suffixierten Keychain-Eintrag (nur bei ungenutztem Pool, Warn-Dialog), Renew entfernt (Datei-Refresh-Token tot, Keychain hat Vorrang), hasCredentials (apikey-Dateicheck) mit Start-Blockierung. Verbrauch-Tab: usage_stats parst <pool>/projects/*/*.jsonl (Dedup message.id+requestId, Preistabelle Fable 10/50 Opus 5/25 Sonnet 3/15 Haiku 1/5, Cache 1.25x/0.1x), Pool-Zeilen aggregiert + Projekte aufklappbar; apiDefault hat keinerlei Transcripts (Marcus' apiKey-Abfragen vom 04.07. liefen nachweislich nicht über claude-code auf diesem Rechner — Console zeigt die Wahrheit). Projekt-Wizard (create_project_full: Scaffold nach 08-07-Muster inkl. pool-guard-Hook, Arbeitsordner anlegen/verknüpfen) + Löschen (Guard bei laufender Session, optional Arbeitsordner aus additionalDirectories mit). Todo-Feature nur Projektebene (robotunits-Muster: OFFENE-PUNKTE.md + jq-SessionStart-Hook, zuschaltbar in Wizard+Zahnrad; Pool-Ebene bewusst nicht). App heißt ai-control (Cargo-Paket umbenannt), Hand-Icon (Tray per include_bytes + Template-Icon; Dock-Icon der Terminals via NSApplication nur in RunEvent::Ready). 38 Rust-Tests. Offen/geplant: Baustein-Bibliothek (CLAUDE.md-Marker-Mechanismus, Konzept steht: ~/.config/ai-control/blocks/, verwaltete Abschnitte zwischen ai-control:block-Markern), weitere Wizard-Optionen, Split privates Repo / offizielles GitHub-Repo (Ist-Stand als Initial-Commit ohne History; OAuth-Login-Teil wegen ToS-Grauzone fürs öffentliche Repo rausnehmen — apiKey-Teil unkritisch), Vitest für UI-Tests (Vorschlag steht), Dock-Name macOS/Wayland „später überlegen".
- Alter Zwischenstand (Commit f747379 = Terminal + Start/Beenden-Button): (a) ProjectList: ein Button pro Zeile — grün „Starten" (open_terminal) / rot „Beenden" (neues Command stop_project: SIGTERM auf exakte PIDs per `pgrep -f -- "--terminal <projekt>$"`, sonst Ghostty-Quit; NIE Quit über die App-Bundle-ID — träfe die Haupt-App); is_running = Ghostty ODER --terminal-Prozess; Status-Kreis ohne Prozess unsichtbar. (b) Terminal-Config-Dialog (Zahnrad): `terminal: { theme, icon, title }` in projekt-`ai-control.json` (pool-Feld jetzt Option, Datei bleibt bei unassign wenn terminal gesetzt); 5 Themes (mocha/dracula/solarized-dark/gruvbox/one-dark, Paletten in terminal.ts, Fenster-BG gespiegelt in terminal.rs theme_background — muss synchron bleiben); Icon = PNG/ICNS-Pfad via tauri-plugin-dialog-Picker, gesetzt per NSApplication.applicationIconImage (objc2, nur in RunEvent::Ready — in setup() überschreibt macOS es wieder); Titel: Fenstertitel + Header, leer → Projektname. `tauri::generate_context!` darf nur 1× expandieren (_EMBED_INFO_PLIST, fällt nur im Release-Build auf).
- Dock-Grenze macOS: Hover-Label im Dock = Bundle-Name, zur Laufzeit nicht öffentlich änderbar; Wrapper-Bundle pro Projekt von Marcus abgelehnt („verlieren zu viel"). Windows/X11: Titel+Icon greifen fensterbasiert; Wayland-Docks bräuchten .desktop pro Projekt (xdg-toplevel-icon nur KDE). → „da müssen wir was überlegen, nicht jetzt".

Frische Pools zeigen Promo-/Upsell-Banner („Werbung"), privateDefault nicht — Ursache ungeklärt (Stand 2026-07-04, nicht weiter vertiefen): Hochsetzen der Zähler in der Pool-`.claude.json` (`tipsHistory.fable-5-promo-2`, `passesUpsellSeenCount`) half nicht, ebenso wenig `cachedGrowthBookFeatures.tengu_birch_lantern = "off"` (einziger Flag-Unterschied der Pools). Kein Settings-Switch vorhanden.

**Agenda 2026-07-05:** Sync-Test auf neuem Mac (pool-Repo mit Symlink-Runtime: clonen, `link-pool-runtime` bzw. Pool-Anlage über die App, /resume-Übernahme prüfen).

**Why:** Getrennte Zugänge pro Pool, [[multi-account-keychain]].

**Agenda 2026-07-04:** Start/Stopp-Architektur grundsätzlich besprechen — Stand vom 2026-07-03 spät (wincount/kill-9/Sentinel-Reste) funktioniert im Test, ist aber Flickwerk um Ghosttys unzuverlässiges Kill-Verhalten herum; Marcus will eine saubere Lösung statt Workarounds. Außerdem: Pool-Guard + Kaltstart über Starter testen; App: UI aufräumen, Projekte managen, Defaults/Standardverhalten (z. B. Todolist); Linux gesondert; Open-Source-Frage („works for my workflow") entschieden nach Feinschliff; außerdem Ghostty und Zed besprechen. Stand App: beta-0.8 gepusht (Tray statt Dock, Autostart-Schalter, Fenster startet unsichtbar, Restart-Dialog).

**How to apply — offen (Stand 2026-07-03):** (1) Sync-Konzept für `~/.config/ai-control` inkl. Unterhaltungen, Secrets-Frage (git). (2) UI kennt die drei neuen Commands nicht. (3) Erledigt durch Integration in `claude-sync` (2026-07-04): der „Flicker" betraf das gelöschte `claude-start`; `claude-sync` läuft in den Ghostty-Wrappern nachweislich interaktiv.


1	---
2	name: multi-account-keychain
3	description: "Multi-Account auf macOS — Stand 2.1.199: pro CLAUDE_CONFIG_DIR eigener suffixierter Keychain-Eintrag; .credentials.json-Ansatz überholt"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: e108ad75-f176-43b6-901d-c3ec19ec3961
8	---
9
10	ÜBERHOLT (galt bis Version vor 2.1.199): claude las die `.credentials.json` direkt aus `CLAUDE_CONFIG_DIR` (verifiziert 2026-07-03 vormittags, Test `bin/oauth-configdir-test`).
11
12	Stand 2026-07-03 abends, Version 2.1.199 (native Installation): Neu-Login war nötig. Die `.credentials.json` im Pool `Mx9Privat` ist weg; claude speichert jetzt pro Config-Dir einen Keychain-Eintrag `Claude Code-credentials-<suffix>`, Suffix = erste 8 Hex-Zeichen von SHA-256 über den `CLAUDE_CONFIG_DIR`-Pfad (Mx9Privat → `a014e206`, TestWürth → erwartet `4c0ad9a9`, ggf. NFC/NFD-Frage wegen „ü"). Ohne gesetztes `CLAUDE_CONFIG_DIR` entsteht der unsuffixierte Eintrag `Claude Code-credentials` plus `~/.claude`/`~/.claude.json`. Damit ist #20553 (Überschreiben) upstream gelöst — pro Pool ein eigener Eintrag. Verifiziert 2026-07-03 abends: Nach `/login` in ai-control (Mx9Privat) lief wuerth-plato (gleicher Pool) ohne erneutes Login — Eintrag `a014e206` wird poolweit geteilt. Keychain enthielt danach nur diesen einen Claude-Code-Eintrag (kein unsuffixierter). Zweiter Pool ebenfalls verifiziert: 08-07 (TestWürth, oauth) verlangte wie erwartet erneutes Login und erzeugte `Claude Code-credentials-4c0ad9a9` — Suffix stimmt mit der Vorab-Berechnung überein, NFC/NFD-Frage wegen „ü" damit geklärt. Keychain-Stand: genau zwei Einträge, `a014e206` (Mx9Privat) und `4c0ad9a9` (TestWürth). Multi-Account über suffixierte Keychain-Einträge funktioniert damit vollständig. Dritter Beleg: robotunits nach Umzug auf TestWürth ohne Login gestartet (Starter-Pfad hasht identisch auf `4c0ad9a9`). Mit gesetztem `CLAUDE_CONFIG_DIR` entsteht dabei kein neues `~/.claude` im Home — das gelöschte bleibt weg.
13
14	Umbenennung 2026-07-03 spätabends: Mx9Privat → privateDefault, TestWürth → apiDefault. Suffixe: privateDefault → `096c4ef9`, apiDefault → `15e10251`. Die alten Einträge `a014e206`/`4c0ad9a9` wurden gelöscht; kurz darauf lag `096c4ef9` im Keychain — eine laufende Session hat die Credentials unter dem neuen Pfad selbst neu geschrieben, ohne Login-Prompt. Weitere Session-Starts erzeugten keinen zusätzlichen Eintrag. Keychain-Endstand 2026-07-03: genau ein Eintrag `096c4ef9`; für apiDefault (`15e10251`) existiert noch keiner, dort ist beim ersten Start Login nötig.
15
16	Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.
17
18	**Why:** Mehrere Claude-Zugänge (Abo + Kunden-Keys) strikt getrennt auf einem Rechner.
19
20	**How to apply:** Pro Pool ein Ordner als `CLAUDE_CONFIG_DIR` (siehe [[ai-control-pool-setup]]). OAuth: `.credentials.json` (0600) in den Pool-Ordner. API-Key: `apiKeyHelper` in der Pool-`settings.json` auf die Key-Datei. `CLAUDE_CODE_OAUTH_TOKEN`-Export ist unnötig.
21

---
name: multi-account-keychain
description: "Multi-Account auf macOS — Stand 2.1.199: pro CLAUDE_CONFIG_DIR eigener suffixierter Keychain-Eintrag; .credentials.json-Ansatz überholt"
metadata:
  node_type: memory
  type: project
  originSessionId: e108ad75-f176-43b6-901d-c3ec19ec3961
---

ÜBERHOLT (galt bis Version vor 2.1.199): claude las die `.credentials.json` direkt aus `CLAUDE_CONFIG_DIR` (verifiziert 2026-07-03 vormittags, Test `bin/oauth-configdir-test`).

Stand 2026-07-03 abends, Version 2.1.199 (native Installation): Neu-Login war nötig. Die `.credentials.json` im Pool `Mx9Privat` ist weg; claude speichert jetzt pro Config-Dir einen Keychain-Eintrag `Claude Code-credentials-<suffix>`, Suffix = erste 8 Hex-Zeichen von SHA-256 über den `CLAUDE_CONFIG_DIR`-Pfad (Mx9Privat → `a014e206`, TestWürth → erwartet `4c0ad9a9`, ggf. NFC/NFD-Frage wegen „ü"). Ohne gesetztes `CLAUDE_CONFIG_DIR` entsteht der unsuffixierte Eintrag `Claude Code-credentials` plus `~/.claude`/`~/.claude.json`. Damit ist #20553 (Überschreiben) upstream gelöst — pro Pool ein eigener Eintrag. Verifiziert 2026-07-03 abends: Nach `/login` in ai-control (Mx9Privat) lief wuerth-plato (gleicher Pool) ohne erneutes Login — Eintrag `a014e206` wird poolweit geteilt. Keychain enthielt danach nur diesen einen Claude-Code-Eintrag (kein unsuffixierter). Zweiter Pool ebenfalls verifiziert: 08-07 (TestWürth, oauth) verlangte wie erwartet erneutes Login und erzeugte `Claude Code-credentials-4c0ad9a9` — Suffix stimmt mit der Vorab-Berechnung überein, NFC/NFD-Frage wegen „ü" damit geklärt. Keychain-Stand: genau zwei Einträge, `a014e206` (Mx9Privat) und `4c0ad9a9` (TestWürth). Multi-Account über suffixierte Keychain-Einträge funktioniert damit vollständig. Dritter Beleg: robotunits nach Umzug auf TestWürth ohne Login gestartet (Starter-Pfad hasht identisch auf `4c0ad9a9`). Mit gesetztem `CLAUDE_CONFIG_DIR` entsteht dabei kein neues `~/.claude` im Home — das gelöschte bleibt weg.

Umbenennung 2026-07-03 spätabends: Mx9Privat → privateDefault, TestWürth → apiDefault. Suffixe: privateDefault → `096c4ef9`, apiDefault → `15e10251`. Die alten Einträge `a014e206`/`4c0ad9a9` wurden gelöscht; kurz darauf lag `096c4ef9` im Keychain — eine laufende Session hat die Credentials unter dem neuen Pfad selbst neu geschrieben, ohne Login-Prompt. Weitere Session-Starts erzeugten keinen zusätzlichen Eintrag. Keychain-Endstand 2026-07-03: genau ein Eintrag `096c4ef9`; für apiDefault (`15e10251`) existiert noch keiner, dort ist beim ersten Start Login nötig.

Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.

**Why:** Mehrere Claude-Zugänge (Abo + Kunden-Keys) strikt getrennt auf einem Rechner.

**How to apply:** Pro Pool ein Ordner als `CLAUDE_CONFIG_DIR` (siehe [[ai-control-pool-setup]]). OAuth: `.credentials.json` (0600) in den Pool-Ordner. API-Key: `apiKeyHelper` in der Pool-`settings.json` auf die Key-Datei. `CLAUDE_CODE_OAUTH_TOKEN`-Export ist unnötig.


Ja, geht. Plan (danach Go abwarten):

1. `~/.config/ai-control/pools/privateDefault` → `~/.config/ai-control/pools/private` umbenennen, `name` in dessen `pool.json` anpassen.
2. Alle 7 Projekt-`ai-control.json` auf `"pool": "private"` umstellen.
3. Pool-Repo `~/claude-projects/pool/privateDefault/` → `private/` umbenennen und die Runtime-Symlinks (seit Commit 6816adb) auf den neuen Pfad zeigen lassen — Symlink-Richtung prüfe ich bei der Umsetzung.
4. Keychain: der Suffix hängt am Config-Dir-Pfad, `096c4ef9` greift nach der Umbenennung nicht mehr. Beim letzten Rename hat eine laufende Session den Eintrag unter dem neuen Suffix selbst neu geschrieben; falls das diesmal ausbleibt, ist einmal `/login` nötig. Alten Eintrag anschließend löschen.
5. `installed_plugins.json` im Pool auf alte Pfade prüfen (`installPaths` waren nach der letzten Umbenennung stale).

Risiko: diese Session läuft selbst auf privateDefault — ihr `CLAUDE_CONFIG_DIR` zeigt nach dem Rename ins Leere, sie kann dabei den alten Ordner leer neu anlegen; den räume ich am Ende ggf. weg. Sauberer wäre die Umbenennung ohne laufende Sessions.


mach mal - dauert ja nicht lange danach schauen wir


Bash

ls -la ~/.config/ai-control/pools/ && ls -la ~/.config/ai-control/pools/privateDefault/ | head -30 && cat ~/.config/ai-control/pools/privateDefault/pool.json

Pool-Verzeichnis und pool.json ansehen


grep -l privateDefault ~/claude-projects/*/ai-control.json; ls ~/claude-projects/pool/

Projekt-Zuordnungen und Pool-Repo ansehen


/home/marcuh/claude-projects/08-07/ai-control.json
/home/marcuh/claude-projects/ai-control/ai-control.json
/home/marcuh/claude-projects/misc/ai-control.json
/home/marcuh/claude-projects/limbach/ai-control.json
/home/marcuh/claude-projects/robotunits/ai-control.json
/home/marcuh/claude-projects/wittgenstein/ai-control.json
/home/marcuh/claude-projects/wuerth-plato/ai-control.json
CLAUDE.md
Test3
apiDefault
bin
commands
privateDefault
settings.json


total 32
drwxr-xr-x   5 marcus.hinz  staff    160 Jul  5 10:08 .
drwxr-xr-x   4 marcus.hinz  staff    128 Jul  3 19:11 ..
-rw-r--r--@  1 marcus.hinz  staff  14340 Jul  4 18:17 .DS_Store
drwxr-xr-x  17 marcus.hinz  staff    544 Jul  4 20:02 apiDefault
drwxr-xr-x  22 marcus.hinz  staff    704 Jul  5 11:23 privateDefault
total 144
drwxr-xr-x   22 marcus.hinz  staff    704 Jul  5 11:23 .
drwxr-xr-x    5 marcus.hinz  staff    160 Jul  5 10:08 ..
-rw-r--r--@   1 marcus.hinz  staff   6148 Jul  4 15:59 .DS_Store
-rw-------    1 marcus.hinz  staff  36806 Jul  5 11:23 .claude.json
-rw-------    1 marcus.hinz  staff    391 Jul  4 13:11 .credentials.json
-rw-r--r--    1 marcus.hinz  staff     24 Jul  4 23:48 .last-cleanup
-rw-r--r--    1 marcus.hinz  staff    161 Jul  4 07:53 .last-update-result.json
-rw-r--r--    1 marcus.hinz  staff   4826 Jul  4 19:08 CLAUDE.md
drwxr-xr-x    7 marcus.hinz  staff    224 Jul  5 11:23 backups
drwxr-xr-x    3 marcus.hinz  staff     96 Jul  3 18:26 cache
drwxr-xr-x    3 marcus.hinz  staff     96 Jul  4 18:59 commands
drwxr-xr-x   20 marcus.hinz  staff    640 Jul  5 11:07 file-history
lrwxr-xr-x    1 marcus.hinz  staff     68 Jul  4 20:08 history.jsonl -> /home/marcuh/claude-projects/pool/privateDefault/history.jsonl
drwxr-xr-x    3 marcus.hinz  staff     96 Jul  4 16:14 paste-cache
drwxr-xr-x    9 marcus.hinz  staff    288 Jul  5 11:23 plugins
-rw-r--r--    1 marcus.hinz  staff     60 Jul  3 19:09 pool.json
lrwxr-xr-x    1 marcus.hinz  staff     63 Jul  4 20:08 projects -> /home/marcuh/claude-projects/pool/privateDefault/projects
drwxr-xr-x  148 marcus.hinz  staff   4736 Jul  5 11:23 session-env
drwx------    3 marcus.hinz  staff     96 Jul  5 11:23 sessions
-rw-r--r--    1 marcus.hinz  staff    629 Jul  4 18:59 settings.json
drwxr-xr-x   27 marcus.hinz  staff    864 Jul  5 11:27 shell-snapshots
lrwxr-xr-x    1 marcus.hinz  staff     60 Jul  4 20:08 todos -> /home/marcuh/claude-projects/pool/privateDefault/todos
{
  "name": "privateDefault",
  "credentialType": "oauth"
}


Continue from where you left off.


No response requested.


alles gut?


Kurzer Stand-Check, dann führe ich die Umbenennung aus — bisher war nur die Bestandsaufnahme gelaufen.


ls ~/.config/ai-control/pools/; ls ~/claude-projects/pool/; readlink ~/.config/ai-control/pools/*/history.jsonl 2>/dev/null; echo "CLAUDE_CONFIG_DIR=$CLAUDE_CONFIG_DIR"

Aktuellen Stand von Pool-Runtime und Pool-Repo prüfen


apiDefault
private
privateDefault
CLAUDE.md
Test3
apiDefault
bin
commands
private
settings.json
/home/marcuh/claude-projects/pool/apiDefault/history.jsonl
/home/marcuh/claude-projects/pool/private/history.jsonl
CLAUDE_CONFIG_DIR=/home/marcuh/.config/ai-control/pools/private


cat ~/.config/ai-control/pools/private/pool.json; echo ---; ls -la ~/.config/ai-control/pools/privateDefault/; echo ---; grep -h pool ~/claude-projects/*/ai-control.json | sort | uniq -c

pool.json, Reste in privateDefault und Projekt-Zuordnungen prüfen


python3 -c "import hashlib;p='/home/marcuh/.config/ai-control/pools/private';print(hashlib.sha256(p.encode()).hexdigest()[:8])"; security find-generic-password -s "Claude Code-credentials" 2>&1 | head -1; for s in 096c4ef9 $(python3 -c "import hashlib;print(hashlib.sha256('/home/marcuh/.config/ai-control/pools/private'.encode()).hexdigest()[:8])"); do echo "-- $s"; security find-generic-password -s "Claude Code-credentials-$s" >/dev/null 2>&1 && echo vorhanden || echo fehlt; done

Keychain-Suffix für neuen Pfad berechnen und Einträge prüfen


{
  "name": "private",
  "credentialType": "oauth"
}
---
total 8
drwx------  5 marcus.hinz  staff  160 Jul  5 11:30 .
drwxr-xr-x  6 marcus.hinz  staff  192 Jul  5 11:29 ..
-rw-------  1 marcus.hinz  staff  191 Jul  5 11:30 history.jsonl
drwx------  3 marcus.hinz  staff   96 Jul  5 11:29 projects
drwxr-xr-x  3 marcus.hinz  staff   96 Jul  5 11:29 session-env
---
   7   "pool": "private",


485a513f
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
-- 096c4ef9
vorhanden
-- 485a513f
vorhanden


cat ~/.config/ai-control/pools/privateDefault/history.jsonl; find ~/.config/ai-control/pools/privateDefault/projects ~/.config/ai-control/pools/privateDefault/session-env -type f | head; echo ---; grep -o '"installPath[^,]*' ~/.config/ai-control/pools/private/plugins/installed_plugins.json 2>/dev/null | head

Rest-Ordner-Inhalt und Plugin-installPaths ansehen


ls -la ~/claude-projects/pool/private/projects/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-0b94-4a6c-b223-77d4b85b102a.jsonl 2>/dev/null; ls -la ~/claude-projects/pool/private/projects/-Users-marcus-hinz-claude-projects-ai-control/ | grep eba397b0; wc -l ~/.config/ai-control/pools/privateDefault/projects/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6.jsonl

Prüfen ob Session-Transcript auch unter private existiert


-rw-------   1 marcus.hinz  staff   109498 Jul  5 11:33 eba397b0-eb36-44d0-85c7-70fcc86c17c6.jsonl
      34 /home/marcuh/.config/ai-control/pools/privateDefault/projects/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6.jsonl


rm -rf ~/.config/ai-control/pools/privateDefault && security delete-generic-password -s "Claude Code-credentials-096c4ef9" >/dev/null 2>&1 && echo "Keychain 096c4ef9 gelöscht"; ls ~/.config/ai-control/pools/; security dump-keychain 2>/dev/null | grep -o 'Claude Code-credentials[^"]*' | sort -u

Rest-Ordner privateDefault und alten Keychain-Eintrag löschen, Endstand zeigen


Keychain 096c4ef9 gelöscht
apiDefault
private
Claude Code-credentials-485a513f
Claude Code-credentials-54743385
Claude Code-credentials-550a1032
Claude Code-credentials-b425db93
Claude Code-credentials-dfbfcf1a


Edit

Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.

Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.

Umbenennung 2026-07-05: privateDefault → private (Runtime-Ordner, Repo-Ordner, Symlinks, alle 7 Projekt-ai-control.json). Neuer Suffix `485a513f`, von der laufenden Session ohne Login selbst geschrieben; `096c4ef9` gelöscht. Im Keychain liegen daneben vier ungeklärte Einträge (54743385, 550a1032, b425db93, dfbfcf1a), keinem Pool zugeordnet — vermutlich Test-Reste.


The file /home/marcuh/claude-projects/ai-control/memory/multi-account-keychain.md has been updated successfully. (file state is current in your context — no need to Read it back)

Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.

Umbenennung 2026-07-05: privateDefault → private (Runtime-Ordner, Repo-Ordner, Symlinks, alle 7 Projekt-ai-control.json). Neuer Suffix `485a513f`, von der laufenden Session ohne Login selbst geschrieben; `096c4ef9` gelöscht. Im Keychain liegen daneben vier ungeklärte Einträge (54743385, 550a1032, b425db93, dfbfcf1a), keinem Pool zugeordnet — vermutlich Test-Reste.

---
name: multi-account-keychain
description: "Multi-Account auf macOS — Stand 2.1.199: pro CLAUDE_CONFIG_DIR eigener suffixierter Keychain-Eintrag; .credentials.json-Ansatz überholt"
metadata:
  node_type: memory
  type: project
  originSessionId: e108ad75-f176-43b6-901d-c3ec19ec3961
---

ÜBERHOLT (galt bis Version vor 2.1.199): claude las die `.credentials.json` direkt aus `CLAUDE_CONFIG_DIR` (verifiziert 2026-07-03 vormittags, Test `bin/oauth-configdir-test`).

Stand 2026-07-03 abends, Version 2.1.199 (native Installation): Neu-Login war nötig. Die `.credentials.json` im Pool `Mx9Privat` ist weg; claude speichert jetzt pro Config-Dir einen Keychain-Eintrag `Claude Code-credentials-<suffix>`, Suffix = erste 8 Hex-Zeichen von SHA-256 über den `CLAUDE_CONFIG_DIR`-Pfad (Mx9Privat → `a014e206`, TestWürth → erwartet `4c0ad9a9`, ggf. NFC/NFD-Frage wegen „ü"). Ohne gesetztes `CLAUDE_CONFIG_DIR` entsteht der unsuffixierte Eintrag `Claude Code-credentials` plus `~/.claude`/`~/.claude.json`. Damit ist #20553 (Überschreiben) upstream gelöst — pro Pool ein eigener Eintrag. Verifiziert 2026-07-03 abends: Nach `/login` in ai-control (Mx9Privat) lief wuerth-plato (gleicher Pool) ohne erneutes Login — Eintrag `a014e206` wird poolweit geteilt. Keychain enthielt danach nur diesen einen Claude-Code-Eintrag (kein unsuffixierter). Zweiter Pool ebenfalls verifiziert: 08-07 (TestWürth, oauth) verlangte wie erwartet erneutes Login und erzeugte `Claude Code-credentials-4c0ad9a9` — Suffix stimmt mit der Vorab-Berechnung überein, NFC/NFD-Frage wegen „ü" damit geklärt. Keychain-Stand: genau zwei Einträge, `a014e206` (Mx9Privat) und `4c0ad9a9` (TestWürth). Multi-Account über suffixierte Keychain-Einträge funktioniert damit vollständig. Dritter Beleg: robotunits nach Umzug auf TestWürth ohne Login gestartet (Starter-Pfad hasht identisch auf `4c0ad9a9`). Mit gesetztem `CLAUDE_CONFIG_DIR` entsteht dabei kein neues `~/.claude` im Home — das gelöschte bleibt weg.

Umbenennung 2026-07-03 spätabends: Mx9Privat → privateDefault, TestWürth → apiDefault. Suffixe: privateDefault → `096c4ef9`, apiDefault → `15e10251`. Die alten Einträge `a014e206`/`4c0ad9a9` wurden gelöscht; kurz darauf lag `096c4ef9` im Keychain — eine laufende Session hat die Credentials unter dem neuen Pfad selbst neu geschrieben, ohne Login-Prompt. Weitere Session-Starts erzeugten keinen zusätzlichen Eintrag. Keychain-Endstand 2026-07-03: genau ein Eintrag `096c4ef9`; für apiDefault (`15e10251`) existiert noch keiner, dort ist beim ersten Start Login nötig.

Kaltstart-Test über die umgestellten Starter (2026-07-04): durchgeführt, funktioniert grundsätzlich (kein Login nötig), aber laut Marcus nicht in der gewünschten Qualität — worin genau der Mangel liegt, ist noch nicht festgehalten.

**Why:** Mehrere Claude-Zugänge (Abo + Kunden-Keys) strikt getrennt auf einem Rechner.

**How to apply:** Pro Pool ein Ordner als `CLAUDE_CONFIG_DIR` (siehe [[ai-control-pool-setup]]). OAuth: `.credentials.json` (0600) in den Pool-Ordner. API-Key: `apiKeyHelper` in der Pool-`settings.json` auf die Key-Datei. `CLAUDE_CODE_OAUTH_TOKEN`-Export ist unnötig.

+Umbenennung 2026-07-05: privateDefault → private (Runtime-Ordner, Repo-Ordner, Symlinks, alle 7 Projekt-ai-control.json). Neuer Suffix `485a513f`, von der laufenden Session ohne Login selbst geschrieben; `096c4ef9` gelöscht. Im Keychain liegen daneben vier ungeklärte Einträge (54743385, 550a1032, b425db93, dfbfcf1a), keinem Pool zugeordnet — vermutlich Test-Reste.


[Request interrupted by user]


Die Umbennung ist durch


Ja. In den Memories ist `multi-account-keychain.md` aktualisiert; `ai-control-pool-setup.md` nennt den Pool noch `privateDefault`.


Eine Änderung, dass das nchit nochb mal passoiertr:
Wir unterscheiden zwischen Nmae (Anzeige) und Pad des pools
bei neunalagen verwenden wir eine UUID als Ordenername


Plan (App `~/projects/ai-control`):

1. **Modell:** Ordnername unter `pools/` wird reine ID, `name` in `pool.json` der Anzeigename. Bei Neuanlage: UUID v4 als Ordnername, Anzeigename in `pool.json`.
2. **Referenzen:** Projekt-`ai-control.json` zeigt weiter auf den Ordnernamen (ID). Damit bleibt `claude-sync` unverändert, und Keychain-Suffix, Symlinks und Transcripts hängen nur noch an der stabilen ID.
3. **Umbenennen:** wird zur trivialen Operation — nur `name` in `pool.json` ändern (neue UI-Aktion am Pool). Kein Pfad-, Keychain- oder Symlink-Umzug mehr.
4. **UI:** überall Anzeigename statt Ordnername (Pool-Liste, Zuordnungs-Dropdown, Verbrauch-Tab); die ID erscheint höchstens als Detail-Info.
5. **Bestand:** `private` und `apiDefault` behalten ihre Ordnernamen — sprechende ID stört nicht, Migration auf UUID entfällt.

Betroffene Stellen suche ich bei der Umsetzung im App-Code zusammen (Rust-Commands create_pool/list/unassign/usage_stats + Vue-Komponenten). Go?


ja


Bash

ls /home/marcuh/projects/ai-control/src-tauri/src/ /home/marcuh/projects/ai-control/src/; grep -rn "pool" /home/marcuh/projects/ai-control/src-tauri/src/*.rs -l

App-Code-Struktur ansehen


/home/marcuh/projects/ai-control/src-tauri/src/:
lib.rs
main.rs
terminal.rs

/home/marcuh/projects/ai-control/src/:
App.vue
assets
components
i18n.ts
main.ts
style.css
terminal.ts
/home/marcuh/projects/ai-control/src-tauri/src/lib.rs


wc -l /home/marcuh/projects/ai-control/src-tauri/src/lib.rs /home/marcuh/projects/ai-control/src/components/*.vue; grep -n "fn \|#\[tauri::command\]" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | grep -A1 command | grep "fn " | head -40

Dateigrößen und Command-Liste ermitteln


    1954 /home/marcuh/projects/ai-control/src-tauri/src/lib.rs
     324 /home/marcuh/projects/ai-control/src/components/PoolList.vue
     550 /home/marcuh/projects/ai-control/src/components/ProjectList.vue
     168 /home/marcuh/projects/ai-control/src/components/UsageList.vue
    2996 total
252:fn todo_hook_command(paths: &Paths, name: &str) -> String {
259:fn settings_path(paths: &Paths, name: &str) -> PathBuf {
545:fn project_icon(project: String) -> Result<Option<String>, String> {
996:fn list_projects() -> Result<Vec<Project>, String> {
1001:fn create_project_full(
1021:fn todo_state(project: String) -> Result<bool, String> {
1026:fn set_todo(project: String, enabled: bool) -> Result<(), String> {
1031:fn delete_project(name: String, delete_work_dirs: bool) -> Result<(), String> {
1039:fn project_work_dirs(project: String) -> Result<Vec<String>, String> {
1044:fn list_pools() -> Result<Vec<PoolInfo>, String> {
1049:fn create_oauth_pool(name: String) -> Result<(), String> {
1054:fn create_apikey_pool(name: String, key: String) -> Result<(), String> {
1059:fn delete_pool(name: String) -> Result<(), String> {
1072:fn assign_pool(project: String, pool: String) -> Result<(), String> {
1077:fn unassign_pool(project: String) -> Result<(), String> {
1082:fn usage_stats(days: u32) -> Result<Vec<UsageRow>, String> {
1087:fn set_terminal_config(
1097:fn set_apikey(pool: String, key: String) -> Result<(), String> {
1119:fn keychain_status(pool: String) -> Result<bool, String> {
1127:fn oauth_login(pool: String) -> Result<(), String> {
1169:fn stop_project(project: String) -> Result<(), String> {
1179:fn restart_project(project: String) -> Result<(), String> {
1192:fn sync_setting() -> Result<bool, String> {
1197:fn set_sync_setting(enabled: bool) -> Result<(), String> {
1204:fn link_pool_runtime(pool: String) -> Result<(), String> {


grep -n "PoolInfo\|pool.json\|pool_json\|struct Pool\|fn pool_dir\|pools_dir\|fn create_pool\|fn list_pools_impl\|create_oauth_pool_impl\|create_apikey_pool_impl\|delete_pool_impl\|link_pool_runtime" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | head -40

Pool-relevante Strukturen und Funktionen finden


17:const POOL_FILE: &str = "pool.json";
40:  fn pools_dir(&self) -> PathBuf {
44:  fn pool_dir(&self, pool: &str) -> PathBuf {
45:    self.pools_dir().join(pool)
63:struct Pool {
605:struct PoolInfo {
617:fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
619:  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
639:    pools.push(PoolInfo {
660:fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
720:fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
746:/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
759:  write_pool_json(&dir, name, "apikey")?;
760:  link_pool_runtime_in(paths, name)
764:/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
769:  write_pool_json(&dir, name, "oauth")?;
770:  link_pool_runtime_in(paths, name)
949:  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
1044:fn list_pools() -> Result<Vec<PoolInfo>, String> {
1204:fn link_pool_runtime(pool: String) -> Result<(), String> {
1209:  link_pool_runtime_in(&paths, &pool)
1335:      link_pool_runtime,


30	  }
31
32	  fn projects_dir(&self) -> PathBuf {
33	    self.home.join("claude-projects")
34	  }
35
36	  fn config_dir(&self) -> PathBuf {
37	    self.home.join(".config").join("ai-control")
38	  }
39
40	  fn pools_dir(&self) -> PathBuf {
41	    self.config_dir().join("pools")
42	  }
43
44	  fn pool_dir(&self, pool: &str) -> PathBuf {
45	    self.pools_dir().join(pool)
46	  }
47
48	  fn project_config(&self, project: &str) -> PathBuf {
49	    self.projects_dir().join(project).join(PROJECT_FILE)
50	  }
51	}
52
53	#[derive(Serialize)]
54	struct Project {
55	  name: String,
56	  path: String,
57	  pool: Option<String>,
58	  running: bool,
59	  terminal: TerminalConfig,
60	}
61
62	#[derive(Serialize, Deserialize)]
63	struct Pool {
64	  name: String,
65	  #[serde(rename = "credentialType")]
66	  credential_type: String,
67	}
68
69	#[derive(Serialize, Deserialize, Default)]
70	struct ProjectConfig {
71	  #[serde(default, skip_serializing_if = "Option::is_none")]
72	  pool: Option<String>,
73	  #[serde(default, skip_serializing_if = "TerminalConfig::is_empty")]
74	  terminal: TerminalConfig,
75	}
76
77	#[derive(Serialize, Deserialize, Default, Clone)]
78	pub struct TerminalConfig {
79	  #[serde(default, skip_serializing_if = "Option::is_none")]
80	  pub theme: Option<String>,
81	  #[serde(default, skip_serializing_if = "Option::is_none")]
82	  pub icon: Option<String>,
83	  #[serde(default, skip_serializing_if = "Option::is_none")]
84	  pub title: Option<String>,
85	}
86
87	impl TerminalConfig {
88	  fn is_empty(&self) -> bool {
89	    self.theme.is_none() && self.icon.is_none() && self.title.is_none()

  }

  fn projects_dir(&self) -> PathBuf {
    self.home.join("claude-projects")
  }

  fn config_dir(&self) -> PathBuf {
    self.home.join(".config").join("ai-control")
  }

  fn pools_dir(&self) -> PathBuf {
    self.config_dir().join("pools")
  }

  fn pool_dir(&self, pool: &str) -> PathBuf {
    self.pools_dir().join(pool)
  }

  fn project_config(&self, project: &str) -> PathBuf {
    self.projects_dir().join(project).join(PROJECT_FILE)
  }
}

#[derive(Serialize)]
struct Project {
  name: String,
  path: String,
  pool: Option<String>,
  running: bool,
  terminal: TerminalConfig,
}

#[derive(Serialize, Deserialize)]
struct Pool {
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,
}

#[derive(Serialize, Deserialize, Default)]
struct ProjectConfig {
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pool: Option<String>,
  #[serde(default, skip_serializing_if = "TerminalConfig::is_empty")]
  terminal: TerminalConfig,
}

#[derive(Serialize, Deserialize, Default, Clone)]
pub struct TerminalConfig {
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub theme: Option<String>,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub icon: Option<String>,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub title: Option<String>,
}

impl TerminalConfig {
  fn is_empty(&self) -> bool {
    self.theme.is_none() && self.icon.is_none() && self.title.is_none()


600	}
601
602	// ---------- Pools ----------
603
604	#[derive(Serialize)]
605	struct PoolInfo {
606	  name: String,
607	  #[serde(rename = "credentialType")]
608	  credential_type: String,
609	  projects: Vec<String>,
610	  /// Teilmenge von `projects`, die gerade läuft (dasselbe `is_running` wie die
611	  /// Projektliste). Der Löschen-Dialog sperrt darauf.
612	  running: Vec<String>,
613	  #[serde(rename = "hasCredentials")]
614	  has_credentials: bool,
615	}
616
617	fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
618	  let mut pools = Vec::new();
619	  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
620	  for entry in entries {
621	    let entry = entry.map_err(|e| e.to_string())?;
622	    let cfg_path = entry.path().join(POOL_FILE);
623	    if !cfg_path.is_file() {
624	      continue;
625	    }
626	    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
627	    let pool: Pool =
628	      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
629	    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
630	    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
631	    let has_credentials = match pool.credential_type.as_str() {
632	      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
633	        .map(|s| !s.trim().is_empty())
634	        .unwrap_or(false),
635	      _ => true,
636	    };
637	    let projects = projects_using_pool(paths, &pool.name)?;
638	    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
639	    pools.push(PoolInfo {
640	      projects,
641	      running,
642	      name: pool.name,
643	      credential_type: pool.credential_type,
644	      has_credentials,
645	    });
646	  }
647	  pools.sort_by(|a, b| a.name.cmp(&b.name));
648	  Ok(pools)
649	}
650
651	fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
652	  check_name(name)?;
653	  let dir = paths.pool_dir(name);
654	  if dir.exists() {
655	    return Err(format!("Pool existiert bereits: {name}"));
656	  }
657	  Ok(dir)
658	}
659
660	fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
661	  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
662	  let pool = Pool {
663	    name: name.to_string(),
664	    credential_type: credential_type.to_string(),
665	  };
666	  let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?;
667	  fs::write(dir.join(POOL_FILE), raw + "\n").map_err(|e| e.to_string())
668	}
669
670	fn write_secret_file(path: &PathBuf, content: &str) -> Result<(), String> {
671	  if let Some(parent) = path.parent() {
672	    fs::create_dir_all(parent).map_err(|e| e.to_string())?;
673	  }
674	  fs::write(path, content).map_err(|e| e.to_string())?;
675	  fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())
676	}
677
678	/// Grundausstattung eines Pool-Ordners (= CLAUDE_CONFIG_DIR): settings.json
679	/// (aufgeräumte UI-Defaults + `extra`) und eine CLAUDE.md, die claude als
680	/// User-Scope liest. CLAUDE.md wird nur angelegt, wenn sie fehlt.
681	/// Die Prompt-Vorschläge/Rückkehr-Zusammenfassung werden abgeschaltet — sonst
682	/// erscheint oben die Vorschlagstabelle.
683	fn init_pool_config(
684	  dir: &PathBuf,
685	  extra: serde_json::Value,
686	) -> Result<(), String> {
687	  let mut settings = serde_json::json!({
688	    "promptSuggestionEnabled": false,
689	    "awaySummaryEnabled": false,
690	  });
691	  let base = settings.as_object_mut().unwrap();
692	  if let Some(obj) = extra.as_object() {
693	    for (k, v) in obj {
694	      base.insert(k.clone(), v.clone());
695	    }
696	  }
697	  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
698	  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
699	  fs::write(dir.join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;
700	  let claude_md = dir.join("CLAUDE.md");
701	  if !claude_md.is_file() {
702	    fs::write(&claude_md, "").map_err(|e| e.to_string())?;
703	  }
704	  Ok(())
705	}
706
707	/// Runtime, die pro Pool ins synced Repo gelinkt wird: Transkripte, Todos,
708	/// Prompt-Historie. (name, ist_ordner)
709	const SYNCED_RUNTIME: [(&str, bool); 3] =
710	  [("projects", true), ("todos", true), ("history.jsonl", false)];
711
712	/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo.
713	fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf {
714	  paths.projects_dir().join("pool").join(pool)
715	}
716
717	/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das
718	/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.
719	/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
720	fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
721	  check_name(pool)?;
722	  let src = paths.pool_dir(pool);
723	  let data = pool_data_dir(paths, pool);
724	  for (name, is_dir) in SYNCED_RUNTIME {
725	    let target = data.join(name);
726	    if is_dir {
727	      fs::create_dir_all(&target).map_err(|e| e.to_string())?;
728	    } else {
729	      fs::create_dir_all(&data).map_err(|e| e.to_string())?;
730	      if !target.exists() {
731	        fs::write(&target, "").map_err(|e| e.to_string())?;
732	      }
733	    }
734	    let link = src.join(name);
735	    if link.is_dir() && !link.is_symlink() {
736	      fs::remove_dir_all(&link).map_err(|e| e.to_string())?;
737	    } else if link.is_symlink() || link.exists() {
738	      fs::remove_file(&link).map_err(|e| e.to_string())?;
739	    }
740	    std::os::unix::fs::symlink(&target, &link).map_err(|e| e.to_string())?;
741	  }
742	  Ok(())
743	}
744
745	/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
746	/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
747	fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {
748	  let dir = check_new_pool(paths, name)?;
749	  let key = key.trim();
750	  if key.is_empty() {
751	    return Err("leerer API-Key".into());
752	  }
753	  let key_path = dir.join(APIKEY_FILE);
754	  write_secret_file(&key_path, &format!("{key}\n"))?;
755	  init_pool_config(
756	    &dir,
757	    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
758	  )?;
759	  write_pool_json(&dir, name, "apikey")?;
760	  link_pool_runtime_in(paths, name)
761	}
762
763	/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
764	/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
765	/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
766	fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
767	  let dir = check_new_pool(paths, name)?;
768	  init_pool_config(&dir, serde_json::json!({}))?;
769	  write_pool_json(&dir, name, "oauth")?;
770	  link_pool_runtime_in(paths, name)
771	}
772
773	/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
774	/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
775	/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
776	fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
777	  let dir = paths.pool_dir(name);
778	  if !dir.join(POOL_FILE).is_file() {
779	    return Err(format!("Pool nicht gefunden: {name}"));
780	  }
781	  for project in projects_using_pool(paths, name)? {
782	    unassign_pool_in(paths, &project)?;
783	  }
784	  fs::remove_dir_all(&dir).map_err(|e| e.to_string())

}

// ---------- Pools ----------

#[derive(Serialize)]
struct PoolInfo {
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,
  projects: Vec<String>,
  /// Teilmenge von `projects`, die gerade läuft (dasselbe `is_running` wie die
  /// Projektliste). Der Löschen-Dialog sperrt darauf.
  running: Vec<String>,
  #[serde(rename = "hasCredentials")]
  has_credentials: bool,
}

fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
  let mut pools = Vec::new();
  let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(POOL_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let pool: Pool =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
    // oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
    // security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
    let has_credentials = match pool.credential_type.as_str() {
      "apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
        .map(|s| !s.trim().is_empty())
        .unwrap_or(false),
      _ => true,
    };
    let projects = projects_using_pool(paths, &pool.name)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  let dir = paths.pool_dir(name);
  if dir.exists() {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  Ok(dir)
}

fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
  let pool = Pool {
    name: name.to_string(),
    credential_type: credential_type.to_string(),
  };
  let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?;
  fs::write(dir.join(POOL_FILE), raw + "\n").map_err(|e| e.to_string())
}

fn write_secret_file(path: &PathBuf, content: &str) -> Result<(), String> {
  if let Some(parent) = path.parent() {
    fs::create_dir_all(parent).map_err(|e| e.to_string())?;
  }
  fs::write(path, content).map_err(|e| e.to_string())?;
  fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())
}

/// Grundausstattung eines Pool-Ordners (= CLAUDE_CONFIG_DIR): settings.json
/// (aufgeräumte UI-Defaults + `extra`) und eine CLAUDE.md, die claude als
/// User-Scope liest. CLAUDE.md wird nur angelegt, wenn sie fehlt.
/// Die Prompt-Vorschläge/Rückkehr-Zusammenfassung werden abgeschaltet — sonst
/// erscheint oben die Vorschlagstabelle.
fn init_pool_config(
  dir: &PathBuf,
  extra: serde_json::Value,
) -> Result<(), String> {
  let mut settings = serde_json::json!({
    "promptSuggestionEnabled": false,
    "awaySummaryEnabled": false,
  });
  let base = settings.as_object_mut().unwrap();
  if let Some(obj) = extra.as_object() {
    for (k, v) in obj {
      base.insert(k.clone(), v.clone());
    }
  }
  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;
  let claude_md = dir.join("CLAUDE.md");
  if !claude_md.is_file() {
    fs::write(&claude_md, "").map_err(|e| e.to_string())?;
  }
  Ok(())
}

/// Runtime, die pro Pool ins synced Repo gelinkt wird: Transkripte, Todos,
/// Prompt-Historie. (name, ist_ordner)
const SYNCED_RUNTIME: [(&str, bool); 3] =
  [("projects", true), ("todos", true), ("history.jsonl", false)];

/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo.
fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf {
  paths.projects_dir().join("pool").join(pool)
}

/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das
/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
  check_name(pool)?;
  let src = paths.pool_dir(pool);
  let data = pool_data_dir(paths, pool);
  for (name, is_dir) in SYNCED_RUNTIME {
    let target = data.join(name);
    if is_dir {
      fs::create_dir_all(&target).map_err(|e| e.to_string())?;
    } else {
      fs::create_dir_all(&data).map_err(|e| e.to_string())?;
      if !target.exists() {
        fs::write(&target, "").map_err(|e| e.to_string())?;
      }
    }
    let link = src.join(name);
    if link.is_dir() && !link.is_symlink() {
      fs::remove_dir_all(&link).map_err(|e| e.to_string())?;
    } else if link.is_symlink() || link.exists() {
      fs::remove_file(&link).map_err(|e| e.to_string())?;
    }
    std::os::unix::fs::symlink(&target, &link).map_err(|e| e.to_string())?;
  }
  Ok(())
}

/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, name)
}

/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  link_pool_runtime_in(paths, name)
}

/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte
/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.
fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = paths.pool_dir(name);
  if !dir.join(POOL_FILE).is_file() {
    return Err(format!("Pool nicht gefunden: {name}"));
  }
  for project in projects_using_pool(paths, name)? {
    unassign_pool_in(paths, &project)?;
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())


grep -n "projects_using_pool\|fn check_name\|fn assign_pool_in\|fn unassign_pool_in\|fn usage_stats_in\|fn keychain_status\|fn oauth_login\|fn set_apikey" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs

Weitere Pool-Verwendungsstellen finden


99:fn running_projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
100:  Ok(projects_using_pool(paths, pool)?
206:fn check_name(name: &str) -> Result<(), String> {
484:fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
495:fn unassign_pool_in(paths: &Paths, project: &str) -> Result<(), String> {
583:fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
637:    let projects = projects_using_pool(paths, &pool.name)?;
781:  for project in projects_using_pool(paths, name)? {
788:fn set_apikey_in(paths: &Paths, pool: &str, key: &str) -> Result<(), String> {
928:fn usage_stats_in(paths: &Paths, days: u32) -> Result<Vec<UsageRow>, String> {
1061:  let running = running_projects_using_pool(&paths, &name)?;
1097:fn set_apikey(pool: String, key: String) -> Result<(), String> {
1119:fn keychain_status(pool: String) -> Result<bool, String> {
1127:fn oauth_login(pool: String) -> Result<(), String> {
1131:  let running = running_projects_using_pool(&paths, &pool)?;
1206:  if !running_projects_using_pool(&paths, &pool)?.is_empty() {


200	      }
201	      running = current;
202	    }
203	  });
204	}
205
206	fn check_name(name: &str) -> Result<(), String> {
207	  if name.trim().is_empty() || name.contains('/') || name.contains("..") {
208	    return Err(format!("ungültiger Name: {name}"));
209	  }
210	  Ok(())
211	}
212
213	fn read_pool(paths: &Paths, pool: &str) -> Result<Pool, String> {
214	  let cfg_path = paths.pool_dir(pool).join(POOL_FILE);
215	  let raw = fs::read_to_string(&cfg_path)
216	    .map_err(|e| format!("{}: {e}", cfg_path.display()))?;
217	  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
218	}
219

      }
      running = current;
    }
  });
}

fn check_name(name: &str) -> Result<(), String> {
  if name.trim().is_empty() || name.contains('/') || name.contains("..") {
    return Err(format!("ungültiger Name: {name}"));
  }
  Ok(())
}

fn read_pool(paths: &Paths, pool: &str) -> Result<Pool, String> {
  let cfg_path = paths.pool_dir(pool).join(POOL_FILE);
  let raw = fs::read_to_string(&cfg_path)
    .map_err(|e| format!("{}: {e}", cfg_path.display()))?;
  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
}


480	  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
481	  fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
482	}
483
484	fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
485	  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
486	    return Err(format!("Pool existiert nicht: {pool}"));
487	  }
488	  let mut cfg = read_project_config_in(paths, project)?;
489	  cfg.pool = Some(pool.to_string());
490	  write_project_config_in(paths, project, &cfg)
491	}
492
493	/// Nimmt den Pool raus; die Config-Datei bleibt nur, wenn sie noch
494	/// Terminal-Einstellungen trägt.
495	fn unassign_pool_in(paths: &Paths, project: &str) -> Result<(), String> {
496	  let mut cfg = read_project_config_in(paths, project)?;
497	  cfg.pool = None;
498	  if cfg.terminal.is_empty() {
499	    let cfg_path = paths.project_config(project);
500	    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
501	  }
502	  write_project_config_in(paths, project, &cfg)
503	}
504
505	fn set_terminal_config_in(
506	  paths: &Paths,
507	  project: &str,
508	  mut terminal: TerminalConfig,
509	) -> Result<(), String> {

  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
  fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
}

fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
    return Err(format!("Pool existiert nicht: {pool}"));
  }
  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = Some(pool.to_string());
  write_project_config_in(paths, project, &cfg)
}

/// Nimmt den Pool raus; die Config-Datei bleibt nur, wenn sie noch
/// Terminal-Einstellungen trägt.
fn unassign_pool_in(paths: &Paths, project: &str) -> Result<(), String> {
  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = None;
  if cfg.terminal.is_empty() {
    let cfg_path = paths.project_config(project);
    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
  }
  write_project_config_in(paths, project, &cfg)
}

fn set_terminal_config_in(
  paths: &Paths,
  project: &str,
  mut terminal: TerminalConfig,
) -> Result<(), String> {


583	fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
584	  let mut users = Vec::new();
585	  let entries = fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())?;
586	  for entry in entries {
587	    let entry = entry.map_err(|e| e.to_string())?;
588	    let cfg_path = entry.path().join(PROJECT_FILE);
589	    if !cfg_path.is_file() {
590	      continue;
591	    }
592	    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
593	    let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
594	    if cfg.pool.as_deref() == Some(pool) {
595	      users.push(entry.file_name().to_string_lossy().into_owned());
596	    }
597	  }
598	  users.sort();
599	  Ok(users)
600	}

fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
  let mut users = Vec::new();
  let entries = fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let cfg_path = entry.path().join(PROJECT_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
    if cfg.pool.as_deref() == Some(pool) {
      users.push(entry.file_name().to_string_lossy().into_owned());
    }
  }
  users.sort();
  Ok(users)
}


920	    + output as f64 / 1e6 * ro
921	    + cache_write as f64 / 1e6 * rw
922	    + cache_read as f64 / 1e6 * rr;
923	}
924
925	/// Aggregiert message.usage aus den Transcript-JSONLs aller Pools:
926	/// <pool>/projects/<kodierter-projektpfad>/*.jsonl → Pool × Projekt.
927	/// Sichtfenster ist, was claude noch nicht weggeräumt hat (cleanupPeriodDays).
928	fn usage_stats_in(paths: &Paths, days: u32) -> Result<Vec<UsageRow>, String> {
929	  let now = SystemTime::now()
930	    .duration_since(UNIX_EPOCH)
931	    .map_err(|e| e.to_string())?
932	    .as_secs() as i64;
933	  let cutoff = now - days as i64 * 86400;
934
935	  // kodierter Projektpfad → Projektname
936	  let mut names = HashMap::new();
937	  for entry in fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())? {
938	    let entry = entry.map_err(|e| e.to_string())?;
939	    if !entry.path().join(".claude").is_dir() {
940	      continue;
941	    }
942	    let name = entry.file_name().to_string_lossy().into_owned();
943	    names.insert(encode_project_path(&entry.path().to_string_lossy()), name);
944	  }
945
946	  let mut seen = HashSet::new();
947	  let mut rows: HashMap<(String, String), UsageTotals> = HashMap::new();
948
949	  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
950	    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
951	    let pool = pool_entry.file_name().to_string_lossy().into_owned();
952	    let projects_root = pool_entry.path().join("projects");
953	    if !projects_root.is_dir() {
954	      continue;
955	    }
956	    for proj_entry in fs::read_dir(&projects_root).map_err(|e| e.to_string())? {
957	      let proj_entry = proj_entry.map_err(|e| e.to_string())?;
958	      if !proj_entry.path().is_dir() {
959	        continue;
960	      }
961	      let encoded = proj_entry.file_name().to_string_lossy().into_owned();
962	      // Unbekannte Ordner (Sessions außerhalb der Projekte) unter dem
963	      // kodierten Namen ausweisen statt verschlucken.
964	      let project = names.get(&encoded).cloned().unwrap_or(encoded);
965	      let totals = rows.entry((pool.clone(), project)).or_default();
966	      for file in fs::read_dir(proj_entry.path()).map_err(|e| e.to_string())? {
967	        let file = file.map_err(|e| e.to_string())?;
968	        let path = file.path();
969	        if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
970	          continue;
971	        }
972	        let f = fs::File::open(&path).map_err(|e| format!("{}: {e}", path.display()))?;
973	        for line in BufReader::new(f).lines() {
974	          let line = line.map_err(|e| format!("{}: {e}", path.display()))?;
975	          add_usage_line(&line, cutoff, &mut seen, totals);
976	        }
977	      }
978	    }
979	  }
980
981	  let mut out: Vec<UsageRow> = rows
982	    .into_iter()
983	    .map(|((pool, project), totals)| UsageRow { pool, project, totals })
984	    .collect();
985	  out.sort_by(|a, b| {
986	    a.pool
987	      .cmp(&b.pool)
988	      .then(b.totals.cost_usd.total_cmp(&a.totals.cost_usd))
989	  });
990	  Ok(out)
991	}
992
993	// ---------- Tauri-Commands ----------
994
995	#[tauri::command]
996	fn list_projects() -> Result<Vec<Project>, String> {
997	  list_projects_in(&Paths::real())
998	}
999

    + output as f64 / 1e6 * ro
    + cache_write as f64 / 1e6 * rw
    + cache_read as f64 / 1e6 * rr;
}

/// Aggregiert message.usage aus den Transcript-JSONLs aller Pools:
/// <pool>/projects/<kodierter-projektpfad>/*.jsonl → Pool × Projekt.
/// Sichtfenster ist, was claude noch nicht weggeräumt hat (cleanupPeriodDays).
fn usage_stats_in(paths: &Paths, days: u32) -> Result<Vec<UsageRow>, String> {
  let now = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .map_err(|e| e.to_string())?
    .as_secs() as i64;
  let cutoff = now - days as i64 * 86400;

  // kodierter Projektpfad → Projektname
  let mut names = HashMap::new();
  for entry in fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())? {
    let entry = entry.map_err(|e| e.to_string())?;
    if !entry.path().join(".claude").is_dir() {
      continue;
    }
    let name = entry.file_name().to_string_lossy().into_owned();
    names.insert(encode_project_path(&entry.path().to_string_lossy()), name);
  }

  let mut seen = HashSet::new();
  let mut rows: HashMap<(String, String), UsageTotals> = HashMap::new();

  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
    let pool = pool_entry.file_name().to_string_lossy().into_owned();
    let projects_root = pool_entry.path().join("projects");
    if !projects_root.is_dir() {
      continue;
    }
    for proj_entry in fs::read_dir(&projects_root).map_err(|e| e.to_string())? {
      let proj_entry = proj_entry.map_err(|e| e.to_string())?;
      if !proj_entry.path().is_dir() {
        continue;
      }
      let encoded = proj_entry.file_name().to_string_lossy().into_owned();
      // Unbekannte Ordner (Sessions außerhalb der Projekte) unter dem
      // kodierten Namen ausweisen statt verschlucken.
      let project = names.get(&encoded).cloned().unwrap_or(encoded);
      let totals = rows.entry((pool.clone(), project)).or_default();
      for file in fs::read_dir(proj_entry.path()).map_err(|e| e.to_string())? {
        let file = file.map_err(|e| e.to_string())?;
        let path = file.path();
        if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
          continue;
        }
        let f = fs::File::open(&path).map_err(|e| format!("{}: {e}", path.display()))?;
        for line in BufReader::new(f).lines() {
          let line = line.map_err(|e| format!("{}: {e}", path.display()))?;
          add_usage_line(&line, cutoff, &mut seen, totals);
        }
      }
    }
  }

  let mut out: Vec<UsageRow> = rows
    .into_iter()
    .map(|((pool, project), totals)| UsageRow { pool, project, totals })
    .collect();
  out.sort_by(|a, b| {
    a.pool
      .cmp(&b.pool)
      .then(b.totals.cost_usd.total_cmp(&a.totals.cost_usd))
  });
  Ok(out)
}

// ---------- Tauri-Commands ----------

#[tauri::command]
fn list_projects() -> Result<Vec<Project>, String> {
  list_projects_in(&Paths::real())
}


1
152
153	<template>
154

155	    <button class="primary" :disabled="busy" @click="openNew('oauth')">
156	      {{ $t("pools.newOauth") }}
157	    </button>
158	    <button class="primary" :disabled="busy" @click="openNew('apikey')">
159	      {{ $t("pools.newApikey") }}
160	    </button>
161

162
163
{{ error }}

164
165

166	    <colgroup>
167	      <col />
168	      <col class="col-type" />
169	      <col class="col-projects" />
170	      <col class="col-actions-wide" />
171	    </colgroup>
172

173

174	        {{ $t("pools.pool") }}
175	        {{ $t("pools.type") }}
176
177
178

179

180

181

182
183	          {{ p.name }}
184
185
186	          {{ p.credentialType }}
187
188
189
190	            {{ $t("pools.assigned", p.projects.length) }}
191
192
193	                {{ name }}
194
195
196
197
198
199
200	            <template v-if="p.credentialType === 'oauth'">
201	              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
202	                {{ $t("pools.relogin") }}
203	              </button>
204	            </template>
205	            <template v-else>
206	              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
207	                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
208	              </button>
209	            </template>
210
211	              <button
212	                class="danger"
213	                :disabled="busy || p.running.length > 0"
214	                @click="askDelete(p.name)"
215	              >
216	                {{ $t("pools.delete") }}
217	              </button>
218
219
220	                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}
221
222
223
224
225
226

227

228

229
{{ $t("pools.empty") }}

230
231

232	    <form class="dialog" @submit.prevent="submit">
233
{{ dialogTitle }}

234
235
236	        {{ $t("pools.name") }}
237	        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />
238
239
240
241	        {{ $t("pools.apiKey") }}
242	        <input
243	          v-model="dKey"
244	          type="password"
245	          placeholder="sk-ant-api03-…"
246	          :autofocus="dialog.editing"
247	          required
248	        />
249
250
251

252	        {{ $t("pools.oauthHint") }}
253

254
255

256	        <button type="button" :disabled="busy" @click="closeDialog">
257	          {{ $t("pools.cancel") }}
258	        </button>
259	        <button type="submit" class="primary" :disabled="busy">
260	          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
261	        </button>
262

263	    </form>
264

265
266

271

272
{{ $t("pools.reloginTitle", { name: reloginPool }) }}

273	      <template v-if="reloginRunning.length">
274
{{ $t("pools.reloginBlocked") }}

275

276
{{ name }}

277

278

279	          <button type="button" @click="reloginPool = null">
280	            {{ $t("pools.cancel") }}
281	          </button>
282

283	      </template>
284	      <template v-else>
285

286	          {{
287	            reloginHasEntry
288	              ? $t("pools.reloginWarning", { name: reloginPool })
289	              : $t("pools.reloginNoEntry")
290	          }}
291

292

293	          <button type="button" :disabled="busy" @click="reloginPool = null">
294	            {{ $t("pools.cancel") }}
295	          </button>
296	          <button class="primary" :disabled="busy" @click="confirmRelogin">
297	            {{ $t("pools.reset") }}
298	          </button>
299

300	      </template>
301

302

303
304

305

306
{{ $t("pools.deletePool") }}

307
{{ $t("pools.deleteWarning", { name: deleteName }) }}

308	      <template v-if="deleteProjects.length">
309
{{ $t("pools.deleteUnassigns") }}

310

311
{{ name }}

312

313	      </template>
314

315	        <button type="button" :disabled="busy" @click="deleteName = null">
316	          {{ $t("pools.cancel") }}
317	        </button>
318	        <button class="danger" :disabled="busy" @click="confirmDelete">
319	          {{ $t("pools.delete") }}
320	        </button>
321

322

323

324	</template>
325

<template>

    <button class="primary" :disabled="busy" @click="openNew('oauth')">
      {{ $t("pools.newOauth") }}
    </button>
    <button class="primary" :disabled="busy" @click="openNew('apikey')">
      {{ $t("pools.newApikey") }}
    </button>

{{ error }}

    <colgroup>
      <col />
      <col class="col-type" />
      <col class="col-projects" />
      <col class="col-actions-wide" />
    </colgroup>

        {{ $t("pools.pool") }}
        {{ $t("pools.type") }}

          {{ p.name }}

          {{ p.credentialType }}

            {{ $t("pools.assigned", p.projects.length) }}

                {{ name }}

            <template v-if="p.credentialType === 'oauth'">
              <button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
                {{ $t("pools.relogin") }}
              </button>
            </template>
            <template v-else>
              <button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
                {{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
              </button>
            </template>

              <button
                class="danger"
                :disabled="busy || p.running.length > 0"
                @click="askDelete(p.name)"
              >
                {{ $t("pools.delete") }}
              </button>

                  {{ $t("pools.deleteBlockedTooltip", { projects: p.running.join(", ") }) }}

{{ $t("pools.empty") }}

    <form class="dialog" @submit.prevent="submit">

{{ dialogTitle }}

        {{ $t("pools.name") }}
        <input v-model="dName" placeholder="z. B. privateDefault" autofocus required />

        {{ $t("pools.apiKey") }}
        <input
          v-model="dKey"
          type="password"
          placeholder="sk-ant-api03-…"
          :autofocus="dialog.editing"
          required
        />

        {{ $t("pools.oauthHint") }}

        <button type="button" :disabled="busy" @click="closeDialog">
          {{ $t("pools.cancel") }}
        </button>
        <button type="submit" class="primary" :disabled="busy">
          {{ dialog.mode === "oauth" ? $t("pools.createPool") : $t("pools.save") }}
        </button>

    </form>

{{ $t("pools.reloginTitle", { name: reloginPool }) }}

      <template v-if="reloginRunning.length">

{{ $t("pools.reloginBlocked") }}

{{ name }}

          <button type="button" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>

      </template>
      <template v-else>

          {{
            reloginHasEntry
              ? $t("pools.reloginWarning", { name: reloginPool })
              : $t("pools.reloginNoEntry")
          }}

          <button type="button" :disabled="busy" @click="reloginPool = null">
            {{ $t("pools.cancel") }}
          </button>
          <button class="primary" :disabled="busy" @click="confirmRelogin">
            {{ $t("pools.reset") }}
          </button>

      </template>

{{ $t("pools.deletePool") }}

{{ $t("pools.deleteWarning", { name: deleteName }) }}

      <template v-if="deleteProjects.length">

{{ $t("pools.deleteUnassigns") }}

{{ name }}

      </template>

        <button type="button" :disabled="busy" @click="deleteName = null">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmDelete">
          {{ $t("pools.delete") }}
        </button>

</template>


1
100
101	<template>
102

103

104	      <button :class="{ active: days === 7 }" @click="days = 7">
105	        {{ $t("usage.days7") }}
106	      </button>
107	      <button :class="{ active: days === 30 }" @click="days = 30">
108	        {{ $t("usage.days30") }}
109	      </button>
110

111	    {{ $t("usage.estimateNote") }}
112

113
114
{{ error }}

115
116

117	    <colgroup>
118	      <col />
119	      <col class="col-num" />
120	      <col class="col-num" />
121	      <col class="col-num" />
122	      <col class="col-num" />
123	      <col class="col-cost" />
124	    </colgroup>
125

126

127	        {{ $t("usage.pool") }}
128	        {{ $t("usage.input") }}
129	        {{ $t("usage.output") }}
130	        {{ $t("usage.cacheWrite") }}
131	        {{ $t("usage.cacheRead") }}
132	        {{ $t("usage.cost") }}
133

134

135

136	      <template v-for="g in groups" :key="g.pool">
137

138
139	            {{ open.has(g.pool) ? "▾" : "▸" }}
140	            {{ g.pool }}
141
142	          {{ fmt(g.inputTokens) }}
143	          {{ fmt(g.outputTokens) }}
144	          {{ fmt(g.cacheCreationTokens) }}
145	          {{ fmt(g.cacheReadTokens) }}
146	          {{ cost(g.costUsd) }}
147

148	        <template v-if="open.has(g.pool)">
149

150	            {{ r.project }}
151	            {{ fmt(r.inputTokens) }}
152	            {{ fmt(r.outputTokens) }}
153	            {{ fmt(r.cacheCreationTokens) }}
154	            {{ fmt(r.cacheReadTokens) }}
155	            {{ cost(r.costUsd) }}
156

157	        </template>
158	      </template>
159

160

161

162	        {{ $t("usage.total") }}
163	        {{ cost(totalCost) }}
164

165

166

167
{{ $t("usage.empty") }}

168	</template>
169

<template>

      <button :class="{ active: days === 7 }" @click="days = 7">
        {{ $t("usage.days7") }}
      </button>
      <button :class="{ active: days === 30 }" @click="days = 30">
        {{ $t("usage.days30") }}
      </button>

    {{ $t("usage.estimateNote") }}

{{ error }}

    <colgroup>
      <col />
      <col class="col-num" />
      <col class="col-num" />
      <col class="col-num" />
      <col class="col-num" />
      <col class="col-cost" />
    </colgroup>

        {{ $t("usage.pool") }}
        {{ $t("usage.input") }}
        {{ $t("usage.output") }}
        {{ $t("usage.cacheWrite") }}
        {{ $t("usage.cacheRead") }}
        {{ $t("usage.cost") }}

      <template v-for="g in groups" :key="g.pool">

            {{ open.has(g.pool) ? "▾" : "▸" }}
            {{ g.pool }}

          {{ fmt(g.inputTokens) }}
          {{ fmt(g.outputTokens) }}
          {{ fmt(g.cacheCreationTokens) }}
          {{ fmt(g.cacheReadTokens) }}
          {{ cost(g.costUsd) }}

        <template v-if="open.has(g.pool)">

            {{ r.project }}
            {{ fmt(r.inputTokens) }}
            {{ fmt(r.outputTokens) }}
            {{ fmt(r.cacheCreationTokens) }}
            {{ fmt(r.cacheReadTokens) }}
            {{ cost(r.costUsd) }}

        </template>
      </template>

        {{ $t("usage.total") }}
        {{ cost(totalCost) }}

{{ $t("usage.empty") }}

</template>


grep -n "pool\|Pool" /home/marcuh/projects/ai-control/src/components/ProjectList.vue | head -40; grep -n "uuid" /home/marcuh/projects/ai-control/src-tauri/Cargo.toml; grep -rn "pool" /home/marcuh/projects/ai-control/src/App.vue | head

Pool-Verwendung in ProjectList/App und Cargo-Abhängigkeiten prüfen


9:  pool: string | null;
14:interface Pool {
20:function poolReady(p: Project): boolean {
21:  if (!p.pool) return false;
22:  const pool = pools.value.find((x) => x.name === p.pool);
23:  return pool !== undefined && pool.hasCredentials;
27:  if (poolReady(p)) return undefined;
28:  return p.pool ? "projects.needsKey" : "projects.needsPool";
32:const pools = ref<Pool[]>([]);
67:    pools.value = await invoke<Pool[]>("list_pools");
103:  const pool = (event.target as HTMLSelectElement).value;
104:  const from = project.pool;
106:    await invoke("assign_pool", { project: project.name, pool });
107:    if (project.running && pool !== from) {
108:      pending.value = { name: project.name, from, to: pool };
200:  pool: string;
214:    pool: "",
244:      pool: w.pool === "" ? null : w.pool,
316:      <col class="col-pool" />
323:        {{ $t("projects.pool") }}
340:          <select :value="p.pool ?? ''" @change="assign(p, $event)">
341:            <option value="" disabled>{{ $t("projects.noPool") }}</option>
342:            <option v-for="pool in pools" :key="pool.name" :value="pool.name">
343:              {{ pool.name }}
370:              :disabled="!poolReady(p)"
390:        {{ $t("projects.pool") }}
391:        <select v-model="wizard.pool">
392:          <option value="">{{ $t("projects.noPool") }}</option>
393:          <option v-for="pool in pools" :key="pool.name" :value="pool.name">
394:            {{ pool.name }}
535:
{{ $t("projects.poolChangeSaved") }}

536:

537:        {{ pending.from ?? $t("projects.noPoolAssigned") }}
/home/marcuh/projects/ai-control/src/App.vue:10:const tab = ref<"projects" | "pools" | "usage">("projects");
/home/marcuh/projects/ai-control/src/App.vue:35:      <button :class="{ active: tab === 'pools' }" @click="tab = 'pools'">
/home/marcuh/projects/ai-control/src/App.vue:36:        {{ $t("app.pools") }}
/home/marcuh/projects/ai-control/src/App.vue:55:    <PoolList v-else-if="tab === 'pools'" />


grep -n "mod tests" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs; sed -n '1380,1450p' /home/marcuh/projects/ai-control/src-tauri/src/lib.rs; grep -n "create_oauth_pool_in\|create_apikey_pool_in\|pool_dir\|pool.json" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | sed -n '1,60p'

Testmodul und Pool-Aufrufstellen ansehen