The file /Users/marcus.hinz/claude-projects/ai-control/memory/ai-control-pool-setup.md has been updated successfully. (file state is current in your context — no need to Read it back)

Frische Pools zeigen Promo-/Upsell-Banner („Werbung": fable-5-promo, Passes-Upsell): gesteuert über Zähler in der Pool-`.claude.json` (`tipsHistory`, `passesUpsellSeenCount`, `fullscreenUpsellSeenCount` — zeigen je 3×, dann Ruhe). Kein Settings-Switch vorhanden (verifiziert 2026-07-04 per Snapshot-Diff misc auf privateDefault vs. Test3); privateDefault ist ausgereizt, deshalb dort aufgeräumte UI.

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

---
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".

**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.

+Frische Pools zeigen Promo-/Upsell-Banner („Werbung": fable-5-promo, Passes-Upsell): gesteuert über Zähler in der Pool-`.claude.json` (`tipsHistory`, `passesUpsellSeenCount`, `fullscreenUpsellSeenCount` — zeigen je 3×, dann Ruhe). Kein Settings-Switch vorhanden (verifiziert 2026-07-04 per Snapshot-Diff misc auf privateDefault vs. Test3); privateDefault ist ausgereizt, deshalb dort aufgeräumte UI.


Im Pool-Setup-Memory festgehalten: Banner hängen an den Zählern in der Pool-`.claude.json` (je 3× anzeigen, dann Ruhe), kein Settings-Switch.


kannst du die counts mal - aus neugier - hochsetzen?


Bash

f=/Users/marcus.hinz/.config/ai-control/pools/Test3/.claude.json
jq '.passesUpsellSeenCount = 3 | .tipsHistory["fable-5-promo-2"] = 99' "$f" > "$f.tmp" && mv "$f.tmp" "$f" && chmod 600 "$f"
jq '{passesUpsellSeenCount, tips: .tipsHistory["fable-5-promo-2"], fullscreenUpsellSeenCount}' "$f"

Upsell-Zähler in Test3 .claude.json hochsetzen


Gesetzt in `Test3/.claude.json`: `passesUpsellSeenCount: 3`, `tipsHistory.fable-5-promo-2: 99` (`fullscreenUpsellSeenCount` stand schon auf 3). Falls die misc-Session auf Test3 noch läuft, schreibt sie beim Beenden ihren eigenen Stand zurück und überschreibt das.


hilft auch nicht


jq '{passesUpsellSeenCount, fullscreenUpsellSeenCount, tipsHistory}' /Users/marcus.hinz/.config/ai-control/pools/Test3/.claude.json

Zählerstände in Test3 nach Neustart prüfen


Die Werte stehen noch (99 / 3 / 3), der Banner kommt trotzdem — die Zähler sind also nicht das, was die Anzeige steuert. Woran sie tatsächlich hängt, weiß ich nicht; aus der `.claude.json` allein ist es nicht ablesbar.


ok -- kannst du mal im pool nach den werten schuaen?


grep -ril -e promo -e upsell -e passes /Users/marcus.hinz/.config/ai-control/pools/Test3/ 2>/dev/null

Test3-Pool nach promo/upsell/passes durchsuchen


jq '.cachedGrowthBookFeatures | keys' /Users/marcus.hinz/.config/ai-control/pools/Test3/.claude.json 2>/dev/null; ls /Users/marcus.hinz/.config/ai-control/pools/Test3/cache/

GrowthBook-Flags und cache-Verzeichnis ansehen


[
  "claude_code_skills_dashboard_enabled_cli",
  "tengu-fable-off-switch",
  "tengu-model-error-overrides",
  "tengu-off-switch",
  "tengu-top-of-feed-tip",
  "tengu_1p_event_batch_config",
  "tengu_agent_list_attach",
  "tengu_alder_compass",
  "tengu_amber_anchor",
  "tengu_amber_flint",
  "tengu_amber_heron",
  "tengu_amber_lark",
  "tengu_amber_lattice",
  "tengu_amber_lynx",
  "tengu_amber_moleskin",
  "tengu_amber_packet",
  "tengu_amber_prism",
  "tengu_amber_quill",
  "tengu_amber_redwood2",
  "tengu_amber_redwood3",
  "tengu_amber_rokovoko",
  "tengu_amber_sentinel",
  "tengu_amber_sextant",
  "tengu_amber_wren",
  "tengu_anchor_tide",
  "tengu_ashen_kelp",
  "tengu_aura_sieve",
  "tengu_auto_mode_config",
  "tengu_auto_mode_default_on",
  "tengu_auto_notice_once",
  "tengu_ax_screen_reader",
  "tengu_bad_survey_transcript_ask_config",
  "tengu_basalt_meadow",
  "tengu_basalt_quill",
  "tengu_basalt_spur",
  "tengu_basalt_sundial",
  "tengu_basalt_tern",
  "tengu_bg_attach_stall_ms",
  "tengu_billiard_aviary",
  "tengu_birch_compass",
  "tengu_birch_kettle",
  "tengu_birch_lantern",
  "tengu_birthday_hat",
  "tengu_blue_coaster",
  "tengu_bramble_lintel",
  "tengu_brass_sled",
  "tengu_brick_follow",
  "tengu_bridge_attestation_enforce",
  "tengu_bridge_attestation_enforce_config",
  "tengu_bridge_min_version",
  "tengu_bridge_poll_interval_config",
  "tengu_bridge_poll_interval_ms",
  "tengu_bridge_repl_v2",
  "tengu_bridge_repl_v2_config",
  "tengu_bridge_requires_action_details",
  "tengu_byte_stream_idle_timeout_ms",
  "tengu_c4e_slash_upsell",
  "tengu_c4w_usage_limit_notifications_enabled",
  "tengu_canary",
  "tengu_ccr_bridge",
  "tengu_ccr_bridge_multi_session",
  "tengu_ccr_bundle_max_bytes",
  "tengu_ccr_bundle_seed_enabled",
  "tengu_ccr_delta_rehydrate",
  "tengu_ccr_post_turn_summary",
  "tengu_ccr_v2_send_events_cli",
  "tengu_cedar_halo",
  "tengu_cedar_hollow_7m",
  "tengu_cedar_lantern",
  "tengu_cedar_plume",
  "tengu_cedar_sundial",
  "tengu_cfc_in_product_permissions",
  "tengu_chair_sermon",
  "tengu_chert_bezel",
  "tengu_chomp_inflection",
  "tengu_chrome_auto_enable",
  "tengu_cinder_almanac",
  "tengu_cinder_plover",
  "tengu_classifier_disabled_surfaces",
  "tengu_classifier_summary_heuristic_emit",
  "tengu_classifier_summary_llm_emit",
  "tengu_claudeai_mcp_connectors",
  "tengu_cloth_snorkel",
  "tengu_cobalt_compass",
  "tengu_cobalt_harbor",
  "tengu_cobalt_heron",
  "tengu_cobalt_lantern",
  "tengu_cobalt_plinth",
  "tengu_cobalt_plinth_direct",
  "tengu_cobalt_plinth_putguard",
  "tengu_cobalt_plinth_reader_persist",
  "tengu_cobalt_raccoon",
  "tengu_cobalt_ridge",
  "tengu_cobalt_thicket",
  "tengu_cobalt_wren",
  "tengu_code_diff_cli",
  "tengu_collage_kaleidoscope",
  "tengu_compact_cache_prefix",
  "tengu_compass_dial",
  "tengu_copper_bridge",
  "tengu_copper_fox",
  "tengu_copper_meadow",
  "tengu_copper_thistle",
  "tengu_copper_wren",
  "tengu_coral_beacon",
  "tengu_coral_fern",
  "tengu_cork_lantern",
  "tengu_cork_m4q",
  "tengu_cowork_chrome_automode_default",
  "tengu_crimson_echo",
  "tengu_crimson_vector",
  "tengu_crystal_beam",
  "tengu_desktop_upsell",
  "tengu_desktop_upsell_v2",
  "tengu_destructive_command_warning",
  "tengu_disable_bypass_permissions_mode",
  "tengu_doorbell_agave",
  "tengu_drift_lantern",
  "tengu_dune_wren",
  "tengu_dunwich_bell",
  "tengu_ember_latch",
  "tengu_ember_trail",
  "tengu_event_sampling_config",
  "tengu_event_watchdog_default_on",
  "tengu_feature_claudified_template",
  "tengu_feature_template",
  "tengu_feedback_survey_config",
  "tengu_fennel_kite",
  "tengu_fennel_kite_model",
  "tengu_fg_left_arrow_agents",
  "tengu_fgts",
  "tengu_file_write_optimization",
  "tengu_fine_survey_transcript_ask_config",
  "tengu_flax_grouse",
  "tengu_fleet_past_sessions",
  "tengu_fleetview_onboarding_v2",
  "tengu_flint_harbor",
  "tengu_flint_harbor_prompt",
  "tengu_flint_harbor_share",
  "tengu_frame_publish_context",
  "tengu_frond_boric",
  "tengu_garnet_finch",
  "tengu_gha_plugin_code_review",
  "tengu_gleaming_fair",
  "tengu_good_survey_transcript_ask_config",
  "tengu_gorse_fathom",
  "tengu_gouda_loop",
  "tengu_grey_step2",
  "tengu_gypsum_kite",
  "tengu_harbor",
  "tengu_harbor_ledger",
  "tengu_harbor_permissions",
  "tengu_harbor_prism",
  "tengu_harbor_willow",
  "tengu_hawthorn_steeple",
  "tengu_hawthorn_window",
  "tengu_hazel_osprey",
  "tengu_hazel_osprey_floor",
  "tengu_herring_clock",
  "tengu_ide_rc_auto_enable",
  "tengu_idle_amber_finch",
  "tengu_immediate_model_command",
  "tengu_jade_anvil_4",
  "tengu_kairos_cron",
  "tengu_kairos_cron_durable",
  "tengu_kairos_github_webhooks",
  "tengu_kairos_input_needed_push",
  "tengu_kairos_loop_dynamic",
  "tengu_kairos_loop_prompt",
  "tengu_kairos_push_notifications",
  "tengu_kestrel_arch",
  "tengu_keybinding_customization_release",
  "tengu_lantern_hearth",
  "tengu_lantern_spool",
  "tengu_lapis_anchor",
  "tengu_lapis_anchor_budget",
  "tengu_lapis_finch",
  "tengu_lapis_thicket",
  "tengu_lichen_compass",
  "tengu_lilac_loom",
  "tengu_linen_osprey",
  "tengu_log_datadog_events",
  "tengu_loggia_carousel",
  "tengu_long_context_survey_probability",
  "tengu_long_context_survey_question_variant",
  "tengu_long_context_survey_threshold",
  "tengu_long_context_survey_trigger_mode",
  "tengu_loud_sugary_rock",
  "tengu_loud_sugary_rock2",
  "tengu_lumen_thicket_q7",
  "tengu_malformed_tool_use_clean_retry",
  "tengu_malort_pedway",
  "tengu_maple_pier",
  "tengu_maple_sundial",
  "tengu_maple_tide",
  "tengu_marble_anvil",
  "tengu_marble_lark",
  "tengu_marble_sandcastle",
  "tengu_marble_whisper",
  "tengu_marble_whisper2",
  "tengu_max_version_config",
  "tengu_mcp_elicitation",
  "tengu_mcp_local_oauth_blocked_hosts",
  "tengu_mcp_normalize_root_combinators",
  "tengu_mcp_path_scoped_permissions",
  "tengu_mcp_retry_failed_remote",
  "tengu_mcp_singleton_unwrap",
  "tengu_mcp_stateless_skip_init",
  "tengu_mcp_subagent_prompt",
  "tengu_md_artifact_styling",
  "tengu_mint_lanes",
  "tengu_miraculo_the_bard",
  "tengu_mocha_barista",
  "tengu_moss_anchor",
  "tengu_moth_copse",
  "tengu_native_cursor",
  "tengu_negative_interaction_transcript_ask_config",
  "tengu_nimble_amber_prose",
  "tengu_non_deferrable_builtins",
  "tengu_noreread_q7m_velvet",
  "tengu_ochre_hollow",
  "tengu_olive_hinge",
  "tengu_onyx_plover",
  "tengu_orchid_mantis",
  "tengu_orchid_mantis_v2",
  "tengu_orchid_trellis",
  "tengu_orford_ness",
  "tengu_osprey_lantern",
  "tengu_otk_slot_v1",
  "tengu_passport_quail",
  "tengu_penguin_mode_promo",
  "tengu_penguins_enabled",
  "tengu_permission_friction",
  "tengu_pewter_brook",
  "tengu_pewter_finch",
  "tengu_pewter_kestrel",
  "tengu_pewter_lantern",
  "tengu_pewter_lark",
  "tengu_pewter_ledger",
  "tengu_pewter_summit",
  "tengu_plan_artifact",
  "tengu_plank_river_frost",
  "tengu_plugin_binary_assets",
  "tengu_plugin_official_mkt_git_fallback",
  "tengu_plum_vx3",
  "tengu_porch_bell_9f",
  "tengu_post_compact_survey",
  "tengu_prism_ledger",
  "tengu_prompt_cache_1h_config",
  "tengu_prompt_cache_diagnostics",
  "tengu_prompt_suggestion",
  "tengu_quartz_heron",
  "tengu_quartz_meadow",
  "tengu_quartz_vireo",
  "tengu_quiet_basalt_echo",
  "tengu_quiet_harbor",
  "tengu_quiet_slate_wren",
  "tengu_quill_harbor",
  "tengu_react_vulnerability_warning",
  "tengu_reactive_compact_remote",
  "tengu_read_dedup_killswitch",
  "tengu_red_coaster",
  "tengu_report_findings_tool",
  "tengu_review_bughunter_config",
  "tengu_review_workflow_routing",
  "tengu_rewind_first_message",
  "tengu_riverbed_lantern",
  "tengu_russet_linnet",
  "tengu_saffron_anchor",
  "tengu_saffron_credits_only_tiers",
  "tengu_saffron_lattice",
  "tengu_sage_compass",
  "tengu_sage_compass2",
  "tengu_scarf_coffee",
  "tengu_scratch",
  "tengu_sedge_lantern",
  "tengu_sedge_lantern_holdback",
  "tengu_sepia_cormorant",
  "tengu_sepia_moth",
  "tengu_session_memory",
  "tengu_sessions_elevated_auth_enforcement",
  "tengu_shale_finch",
  "tengu_shining_fractals",
  "tengu_shoji_engine",
  "tengu_silent_harbor",
  "tengu_silk_almanac",
  "tengu_silk_hinge",
  "tengu_skills_dashboard_enabled",
  "tengu_slate_fern",
  "tengu_slate_finch",
  "tengu_slate_harbor_experiment",
  "tengu_slate_harrier",
  "tengu_slate_ibis",
  "tengu_slate_kestrel",
  "tengu_slate_meadow",
  "tengu_slate_meridian",
  "tengu_slate_moth",
  "tengu_slate_nexus",
  "tengu_slate_quill",
  "tengu_slate_ribbon",
  "tengu_slate_siskin",
  "tengu_slate_thimble",
  "tengu_slate_wren",
  "tengu_slim_subagent_claudemd",
  "tengu_sm_config",
  "tengu_snippet_save",
  "tengu_soft_slate_nudge",
  "tengu_sotto_voce",
  "tengu_sparrow_ledger",
  "tengu_startup_announcements",
  "tengu_startup_notice",
  "tengu_steady_lantern",
  "tengu_streaming_tool_execution2",
  "tengu_sub_nomdrep_q7k",
  "tengu_surreal_dali",
  "tengu_swann_brevity",
  "tengu_system_prompt_global_cache",
  "tengu_tab_read_sep",
  "tengu_tangerine_ladder_boost",
  "tengu_team_discovery",
  "tengu_tense_pond",
  "tengu_tern_alloy",
  "tengu_tide_elm",
  "tengu_timber_lark",
  "tengu_tool_pear",
  "tengu_tool_search_unsupported_models",
  "tengu_trace_lantern",
  "tengu_turtle_carbon",
  "tengu_tussock_oriole",
  "tengu_ultraplan_config",
  "tengu_ultraplan_prompt_identifier",
  "tengu_ultraplan_timeout_seconds",
  "tengu_umber_petrel",
  "tengu_usage_overage_included_models",
  "tengu_vellum_lantern",
  "tengu_vellum_siding",
  "tengu_velvet_hammer",
  "tengu_velvet_hammer_fable_5",
  "tengu_velvet_hammer_falcon",
  "tengu_velvet_hammer_haiku",
  "tengu_velvet_hammer_haiku_4_5",
  "tengu_velvet_hammer_mythos_5",
  "tengu_velvet_hammer_opus",
  "tengu_velvet_hammer_opus_4_6",
  "tengu_velvet_hammer_opus_4_7",
  "tengu_velvet_hammer_opus_4_8",
  "tengu_velvet_hammer_sonnet",
  "tengu_velvet_hammer_sonnet_4_5",
  "tengu_velvet_hammer_sonnet_4_6",
  "tengu_velvet_hammer_sonnet_5",
  "tengu_velvet_harbor",
  "tengu_velvet_ibis",
  "tengu_velvet_mallet",
  "tengu_velvet_mallet_fable_5",
  "tengu_velvet_mallet_falcon",
  "tengu_velvet_mallet_haiku",
  "tengu_velvet_mallet_haiku_4_5",
  "tengu_velvet_mallet_opus",
  "tengu_velvet_mallet_opus_4_6",
  "tengu_velvet_mallet_opus_4_7",
  "tengu_velvet_mallet_opus_4_8",
  "tengu_velvet_mallet_sonnet",
  "tengu_velvet_mallet_sonnet_4_5",
  "tengu_velvet_moth",
  "tengu_velvet_static",
  "tengu_velvet_tide",
  "tengu_version_config",
  "tengu_vscode_feedback_survey",
  "tengu_vscode_onboarding",
  "tengu_vscode_review_upsell",
  "tengu_walnut_prism",
  "tengu_walrus_canteen",
  "tengu_willow_census_ttl_hours",
  "tengu_willow_mode",
  "tengu_willow_refresh_ttl_hours",
  "tengu_willow_sentinel_ttl_hours",
  "tengu_windows_credman",
  "tengu_workflows_enabled",
  "tengu_workout2",
  "tengu_worktree_mode",
  "tengu_xq7_marble_zephyr",
  "tengu_xterm_atlas_reset"
]
changelog.md


diff <(jq -S '.cachedGrowthBookFeatures' /Users/marcus.hinz/.config/ai-control/pools/privateDefault/.claude.json) <(jq -S '.cachedGrowthBookFeatures' /Users/marcus.hinz/.config/ai-control/pools/Test3/.claude.json) | head -60

GrowthBook-Flag-Werte beider Pools diffen


117c117
<   "tengu_birch_lantern": "off",
---
>   "tengu_birch_lantern": "step",


Im Pool liegen die Werte als GrowthBook-Flag-Cache in der `.claude.json` (`cachedGrowthBookFeatures`, ~370 Flags mit verschleierten Namen). Der Diff zwischen beiden Pools ergibt genau einen Unterschied:

```
tengu_birch_lantern:  privateDefault = "off"   Test3 = "step"
```

privateDefault (aufgeräumte UI) hat „off", Test3 (mit Werbung) hat „step". Sonst sind die Flag-Caches identisch.


dann setz das mal


f=/Users/marcus.hinz/.config/ai-control/pools/Test3/.claude.json
jq '.cachedGrowthBookFeatures.tengu_birch_lantern = "off"' "$f" > "$f.tmp" && mv "$f.tmp" "$f" && chmod 600 "$f"
jq '.cachedGrowthBookFeatures.tengu_birch_lantern' "$f"

tengu_birch_lantern in Test3 auf off setzen


"off"


Gesetzt: `tengu_birch_lantern = "off"` in `Test3/.claude.json`. Der Cache wird beim Start vom Server aufgefrischt (`cachedGrowthBookFeaturesAt`) — der Wert kann dabei wieder überschrieben werden.


ist es auch nicht
brauchen wir nicht zu vertiefenn
Morgen :: sync Test auf neuem Mach

alles commiten und pushen


Edit

Frische Pools zeigen Promo-/Upsell-Banner („Werbung": fable-5-promo, Passes-Upsell): gesteuert über Zähler in der Pool-`.claude.json` (`tipsHistory`, `passesUpsellSeenCount`, `fullscreenUpsellSeenCount` — zeigen je 3×, dann Ruhe). Kein Settings-Switch vorhanden (verifiziert 2026-07-04 per Snapshot-Diff misc auf privateDefault vs. Test3); privateDefault ist ausgereizt, deshalb dort aufgeräumte UI.

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).


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).

---
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": fable-5-promo, Passes-Upsell): gesteuert über Zähler in der Pool-`.claude.json` (`tipsHistory`, `passesUpsellSeenCount`, `fullscreenUpsellSeenCount` — zeigen je 3×, dann Ruhe). Kein Settings-Switch vorhanden (verifiziert 2026-07-04 per Snapshot-Diff misc auf privateDefault vs. Test3); privateDefault ist ausgereizt, deshalb dort aufgeräumte UI.

**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.

-Frische Pools zeigen Promo-/Upsell-Banner („Werbung": fable-5-promo, Passes-Upsell): gesteuert über Zähler in der Pool-`.claude.json` (`tipsHistory`, `passesUpsellSeenCount`, `fullscreenUpsellSeenCount` — zeigen je 3×, dann Ruhe). Kein Settings-Switch vorhanden (verifiziert 2026-07-04 per Snapshot-Diff misc auf privateDefault vs. Test3); privateDefault ist ausgereizt, deshalb dort aufgeräumte UI.

+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).


ist es auch nicht brauchen wir nicht zu vertiefenn Morgen :: sync Test auf neuem Mach  alles commiten und pushen


Launching skill: git-sync


Führe genau dieses eine Kommando aus, sonst nichts:

```bash
~/claude-projects/pool/bin/git-sync
```

Das Skript liest selbst die `permissions.additionalDirectories` aus den Settings und
synct jedes Repo (add, commit mit Timestamp-Message, pull --rebase, push). Kein force,
kein Merge. Gib anschließend die Ausgabe des Skripts wieder.


Bash

git-sync über alle konfigurierten Repos ausführen


· ai-control: keine Änderungen.
· ai-control: nichts zu pushen.


cd /Users/marcus.hinz/claude-projects && git add -A && git commit -m "Pool-Runtime als Symlinks im Repo (Test3-Test), Werbung-Analyse ergebnislos, Agenda Sync-Test neuer Mac" && git push

claude-projects-Repo committen und pushen


[main 6816adb] Pool-Runtime als Symlinks im Repo (Test3-Test), Werbung-Analyse ergebnislos, Agenda Sync-Test neuer Mac
 10 files changed, 507 insertions(+), 1 deletion(-)
 create mode 100644 ai-control/memory/feedback-warum-ist-keine-suchlizenz.md
 create mode 100644 pool/Test3/history.jsonl
 create mode 100644 pool/Test3/projects/-Users-marcus-hinz-claude-projects-misc/17390815-0bdd-4410-93e2-41031f28490f.jsonl
 create mode 100644 pool/apiDefault/history.jsonl
 create mode 100644 pool/apiDefault/projects/-Users-marcus-hinz-claude-projects-ai-control/9ca637e1-89b8-4a5f-beaf-f2eab81d2302.jsonl
 create mode 100755 pool/bin/link-pool-runtime
 create mode 100644 pool/privateDefault/history.jsonl
 create mode 100644 pool/privateDefault/projects/-Users-marcus-hinz-claude-projects-ai-control/2824ecea-0b94-4a6c-b223-77d4b85b102a.jsonl
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://git.lintus.art:2222/marcus.hinz/claude-projects.git
   79314d6..6816adb  main -> main
Shell cwd was reset to /Users/marcus.hinz/claude-projects/ai-control


Alles committed und gepusht:

- **App-Repo:** war schon aktuell (`09488ae` von vorhin, git-sync: keine Änderungen).
- **claude-projects:** `6816adb` gepusht — enthält `pool/bin/link-pool-runtime`, die Symlink-Runtime aller drei Pools (inkl. Test3-/misc-Transcript und dem dieser Session) sowie die Memory-Updates. Im Pool-Setup-Memory steht die Agenda für morgen: Sync-Test auf dem neuen Mac.


noch viel zu schauen
schaun wir aml, was ankommt und dann --> linux


Gut. Morgen zeigt der Test auf dem neuen Mac, was über das Repo ankommt — Linux danach.


könne wir noch etwas für layout und Performance tun?
das Terninal ist ok (ligatur geht auc) -- ABER manchaml Wellen in den Wörtern (Über nach unter Linie) - und SCHNELER
Gibt es alternativen?


Drei Hebel bei xterm.js, danach die Alternativen:

**1. WebGL-Renderer (`@xterm/addon-webgl`)** — größter Einzelgewinn für beides. Ohne ihn rendert xterm.js per DOM: jedes Zeichen ein Span, Baseline hängt am Font-Layout der WebView → genau die „Wellen". Der WebGL-Renderer zeichnet aus einem Glyph-Atlas mit fixer Zellgeometrie (Baseline überall identisch) und ist um Größenordnungen schneller (VS Code nutzt denselben Weg). Ligaturen funktionieren damit weiter.

**2. Font-Metrik:** Die Wellen entstehen auch, wenn xterm.js misst, bevor JetBrains Mono geladen ist (Fallback-Metrik bleibt hängen). Vor `term.open()` auf `document.fonts.ready` warten bzw. die Fontface explizit laden; dazu ganzzahlige `fontSize`, `letterSpacing: 0`, `lineHeight: 1.0`.

**3. Output-Pfad:** Aktuell geht jeder PTY-Read als eigenes base64-Event durch die Tauri-IPC — viele kleine Events + Decode sind der Flaschenhals beim Scrollen/Fluten. PTY-Output im Rust-Teil sammeln und alle ~8–16 ms als einen Chunk emittieren, im Frontend als `Uint8Array` an `term.write()` geben. `scrollback` begrenzen.

**Alternativen:**
- Im Web-Stack gibt es real nichts Besseres — hterm (ChromeOS) ist älter und langsamer, sonstige Projekte sind tot. xterm.js + WebGL ist der Stand der Technik.
- Nativ: **libghostty** — Ghosttys Terminal-Kern als embeddable Library (Metal-gerendert, Ligaturen, schnell). Läuft aber als eigene native View, nicht in der WKWebView — wäre ein eigenes Fenster neben der Tauri-App, architektonisch näher am alten Ghostty-Starter als am eingebauten Terminal.

Realistisch: Punkte 1–3 im bestehenden Terminal.


könne wir noch etwas für layout und Performance tun? das Terninal ist ok (ligatur geht auc) -- ABER manchaml Wellen in den Wörtern (Über nach unter Linie) - und SCHNELER Gibt es alternativen?


dann hau mal rein


1	import { Terminal } from "@xterm/xterm";
2	import { FitAddon } from "@xterm/addon-fit";
3	import { WebglAddon } from "@xterm/addon-webgl";
4	import { LigaturesAddon } from "@xterm/addon-ligatures";
5	import "@xterm/xterm/css/xterm.css";
6	import "@fontsource/jetbrains-mono/400.css";
7	import "@fontsource/jetbrains-mono/500.css";
8	import "@fontsource/jetbrains-mono/700.css";
9	import { invoke } from "@tauri-apps/api/core";
10	import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
11
12	// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
13	function showError(msg: string) {
14	  const el = document.createElement("pre");
15	  el.style.cssText =
16	    "color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
17	  el.textContent = msg;
18	  document.body.appendChild(el);
19	  invoke("term_log", { msg });
20	}
21	window.addEventListener("error", (e) =>
22	  showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
23	);
24	window.addEventListener("unhandledrejection", (e) =>
25	  showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
26	);
27
28	const project = new URLSearchParams(location.search).get("project")!;
29	const win = getCurrentWebviewWindow();
30
31	// Fensterhintergründe je Theme stehen zusätzlich in terminal.rs
32	// (theme_background) — beide Stellen müssen zusammenpassen.
33	const THEMES: Record<
34	  string,
35	  { header: string; border: string; xterm: Record<string, string> }
36	> = {
37	  mocha: {
38	    header: "#181825",
39	    border: "#313244",
40	    xterm: {
41	      background: "#1e1e2e",
42	      foreground: "#cdd6f4",
43	      cursor: "#f5e0dc",
44	      cursorAccent: "#1e1e2e",
45	      selectionBackground: "#585b7080",
46	      black: "#45475a",
47	      red: "#f38ba8",
48	      green: "#a6e3a1",
49	      yellow: "#f9e2af",
50	      blue: "#89b4fa",
51	      magenta: "#f5c2e7",
52	      cyan: "#94e2d5",
53	      white: "#bac2de",
54	      brightBlack: "#585b70",
55	      brightRed: "#f38ba8",
56	      brightGreen: "#a6e3a1",
57	      brightYellow: "#f9e2af",
58	      brightBlue: "#89b4fa",
59	      brightMagenta: "#f5c2e7",
60	      brightCyan: "#94e2d5",
61	      brightWhite: "#a6adc8",
62	    },
63	  },
64	  dracula: {
65	    header: "#21222c",
66	    border: "#44475a",
67	    xterm: {
68	      background: "#282a36",
69	      foreground: "#f8f8f2",
70	      cursor: "#f8f8f2",
71	      cursorAccent: "#282a36",
72	      selectionBackground: "#44475a80",
73	      black: "#21222c",
74	      red: "#ff5555",
75	      green: "#50fa7b",
76	      yellow: "#f1fa8c",
77	      blue: "#bd93f9",
78	      magenta: "#ff79c6",
79	      cyan: "#8be9fd",
80	      white: "#f8f8f2",
81	      brightBlack: "#6272a4",
82	      brightRed: "#ff6e6e",
83	      brightGreen: "#69ff94",
84	      brightYellow: "#ffffa5",
85	      brightBlue: "#d6acff",
86	      brightMagenta: "#ff92df",
87	      brightCyan: "#a4ffff",
88	      brightWhite: "#ffffff",
89	    },
90	  },
91	  "solarized-dark": {
92	    header: "#073642",
93	    border: "#586e75",
94	    xterm: {
95	      background: "#002b36",
96	      foreground: "#839496",
97	      cursor: "#839496",
98	      cursorAccent: "#002b36",
99	      selectionBackground: "#07364280",
100	      black: "#073642",
101	      red: "#dc322f",
102	      green: "#859900",
103	      yellow: "#b58900",
104	      blue: "#268bd2",
105	      magenta: "#d33682",
106	      cyan: "#2aa198",
107	      white: "#eee8d5",
108	      brightBlack: "#586e75",
109	      brightRed: "#cb4b16",
110	      brightGreen: "#859900",
111	      brightYellow: "#657b83",
112	      brightBlue: "#839496",
113	      brightMagenta: "#6c71c4",
114	      brightCyan: "#93a1a1",
115	      brightWhite: "#fdf6e3",
116	    },
117	  },
118	  gruvbox: {
119	    header: "#1d2021",
120	    border: "#3c3836",
121	    xterm: {
122	      background: "#282828",
123	      foreground: "#ebdbb2",
124	      cursor: "#ebdbb2",
125	      cursorAccent: "#282828",
126	      selectionBackground: "#3c383680",
127	      black: "#282828",
128	      red: "#cc241d",
129	      green: "#98971a",
130	      yellow: "#d79921",
131	      blue: "#458588",
132	      magenta: "#b16286",
133	      cyan: "#689d6a",
134	      white: "#a89984",
135	      brightBlack: "#928374",
136	      brightRed: "#fb4934",
137	      brightGreen: "#b8bb26",
138	      brightYellow: "#fabd2f",
139	      brightBlue: "#83a598",
140	      brightMagenta: "#d3869b",
141	      brightCyan: "#8ec07c",
142	      brightWhite: "#ebdbb2",
143	    },
144	  },
145	  "one-dark": {
146	    header: "#21252b",
147	    border: "#3e4451",
148	    xterm: {
149	      background: "#282c34",
150	      foreground: "#abb2bf",
151	      cursor: "#abb2bf",
152	      cursorAccent: "#282c34",
153	      selectionBackground: "#3e445180",
154	      black: "#282c34",
155	      red: "#e06c75",
156	      green: "#98c379",
157	      yellow: "#e5c07b",
158	      blue: "#61afef",
159	      magenta: "#c678dd",
160	      cyan: "#56b6c2",
161	      white: "#abb2bf",
162	      brightBlack: "#5c6370",
163	      brightRed: "#e06c75",
164	      brightGreen: "#98c379",
165	      brightYellow: "#e5c07b",
166	      brightBlue: "#61afef",
167	      brightMagenta: "#c678dd",
168	      brightCyan: "#56b6c2",
169	      brightWhite: "#ffffff",
170	    },
171	  },
172	};
173
174	interface Project {
175	  name: string;
176	  pool: string | null;
177	  terminal: { theme: string | null; icon: string | null; title: string | null };
178	}
179	const projects = await invoke<Project[]>("list_projects");
180	const cfg = projects.find((p) => p.name === project);
181	if (cfg?.pool) document.getElementById("pool")!.textContent = cfg.pool;
182	document.getElementById("project-name")!.textContent =
183	  cfg?.terminal.title ?? project;
184
185	const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
186	const theme = picked.xterm;
187
188	// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
189	document.documentElement.style.background = theme.background;
190	document.body.style.background = theme.background;
191	const header = document.querySelector("header") as HTMLElement;
192	header.style.background = picked.header;
193	header.style.borderBottomColor = picked.border;
194	header.style.color = theme.foreground;
195
196	const term = new Terminal({
197	  // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
198	  allowProposedApi: true,
199	  fontFamily: '"JetBrains Mono", Menlo, monospace',
200	  fontSize: 13,
201	  fontWeightBold: "600",
202	  lineHeight: 1.25,
203	  letterSpacing: 0,
204	  cursorBlink: true,
205	  cursorStyle: "bar",
206	  scrollback: 10000,
207	  theme,
208	});
209	const fit = new FitAddon();
210	term.loadAddon(fit);
211
212	// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
213	// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
214	// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
215	term.attachCustomKeyEventHandler((e) => {
216	  if (e.key === "Enter" && e.shiftKey) {
217	    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
218	    return false;
219	  }
220	  return true;
221	});
222
223	// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
224	await document.fonts.load('13px "JetBrains Mono"');
225	term.open(document.getElementById("term")!);
226	// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
227	term.loadAddon(new LigaturesAddon());
228	term.loadAddon(new WebglAddon());
229	fit.fit();
230
231	function b64ToBytes(b64: string): Uint8Array {
232	  const bin = atob(b64);
233	  const bytes = new Uint8Array(bin.length);
234	  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
235	  return bytes;
236	}
237
238	await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
239	await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));
240
241	term.onData((data) => invoke("term_write", { data }));
242	term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
243	window.addEventListener("resize", () => fit.fit());
244
245	await invoke("term_start", { project, rows: term.rows, cols: term.cols });
246	term.focus();
247

import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebglAddon } from "@xterm/addon-webgl";
import { LigaturesAddon } from "@xterm/addon-ligatures";
import "@xterm/xterm/css/xterm.css";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/700.css";
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
function showError(msg: string) {
  const el = document.createElement("pre");
  el.style.cssText =
    "color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
  el.textContent = msg;
  document.body.appendChild(el);
  invoke("term_log", { msg });
}
window.addEventListener("error", (e) =>
  showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
);
window.addEventListener("unhandledrejection", (e) =>
  showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
);

const project = new URLSearchParams(location.search).get("project")!;
const win = getCurrentWebviewWindow();

// Fensterhintergründe je Theme stehen zusätzlich in terminal.rs
// (theme_background) — beide Stellen müssen zusammenpassen.
const THEMES: Record<
  string,
  { header: string; border: string; xterm: Record<string, string> }
> = {
  mocha: {
    header: "#181825",
    border: "#313244",
    xterm: {
      background: "#1e1e2e",
      foreground: "#cdd6f4",
      cursor: "#f5e0dc",
      cursorAccent: "#1e1e2e",
      selectionBackground: "#585b7080",
      black: "#45475a",
      red: "#f38ba8",
      green: "#a6e3a1",
      yellow: "#f9e2af",
      blue: "#89b4fa",
      magenta: "#f5c2e7",
      cyan: "#94e2d5",
      white: "#bac2de",
      brightBlack: "#585b70",
      brightRed: "#f38ba8",
      brightGreen: "#a6e3a1",
      brightYellow: "#f9e2af",
      brightBlue: "#89b4fa",
      brightMagenta: "#f5c2e7",
      brightCyan: "#94e2d5",
      brightWhite: "#a6adc8",
    },
  },
  dracula: {
    header: "#21222c",
    border: "#44475a",
    xterm: {
      background: "#282a36",
      foreground: "#f8f8f2",
      cursor: "#f8f8f2",
      cursorAccent: "#282a36",
      selectionBackground: "#44475a80",
      black: "#21222c",
      red: "#ff5555",
      green: "#50fa7b",
      yellow: "#f1fa8c",
      blue: "#bd93f9",
      magenta: "#ff79c6",
      cyan: "#8be9fd",
      white: "#f8f8f2",
      brightBlack: "#6272a4",
      brightRed: "#ff6e6e",
      brightGreen: "#69ff94",
      brightYellow: "#ffffa5",
      brightBlue: "#d6acff",
      brightMagenta: "#ff92df",
      brightCyan: "#a4ffff",
      brightWhite: "#ffffff",
    },
  },
  "solarized-dark": {
    header: "#073642",
    border: "#586e75",
    xterm: {
      background: "#002b36",
      foreground: "#839496",
      cursor: "#839496",
      cursorAccent: "#002b36",
      selectionBackground: "#07364280",
      black: "#073642",
      red: "#dc322f",
      green: "#859900",
      yellow: "#b58900",
      blue: "#268bd2",
      magenta: "#d33682",
      cyan: "#2aa198",
      white: "#eee8d5",
      brightBlack: "#586e75",
      brightRed: "#cb4b16",
      brightGreen: "#859900",
      brightYellow: "#657b83",
      brightBlue: "#839496",
      brightMagenta: "#6c71c4",
      brightCyan: "#93a1a1",
      brightWhite: "#fdf6e3",
    },
  },
  gruvbox: {
    header: "#1d2021",
    border: "#3c3836",
    xterm: {
      background: "#282828",
      foreground: "#ebdbb2",
      cursor: "#ebdbb2",
      cursorAccent: "#282828",
      selectionBackground: "#3c383680",
      black: "#282828",
      red: "#cc241d",
      green: "#98971a",
      yellow: "#d79921",
      blue: "#458588",
      magenta: "#b16286",
      cyan: "#689d6a",
      white: "#a89984",
      brightBlack: "#928374",
      brightRed: "#fb4934",
      brightGreen: "#b8bb26",
      brightYellow: "#fabd2f",
      brightBlue: "#83a598",
      brightMagenta: "#d3869b",
      brightCyan: "#8ec07c",
      brightWhite: "#ebdbb2",
    },
  },
  "one-dark": {
    header: "#21252b",
    border: "#3e4451",
    xterm: {
      background: "#282c34",
      foreground: "#abb2bf",
      cursor: "#abb2bf",
      cursorAccent: "#282c34",
      selectionBackground: "#3e445180",
      black: "#282c34",
      red: "#e06c75",
      green: "#98c379",
      yellow: "#e5c07b",
      blue: "#61afef",
      magenta: "#c678dd",
      cyan: "#56b6c2",
      white: "#abb2bf",
      brightBlack: "#5c6370",
      brightRed: "#e06c75",
      brightGreen: "#98c379",
      brightYellow: "#e5c07b",
      brightBlue: "#61afef",
      brightMagenta: "#c678dd",
      brightCyan: "#56b6c2",
      brightWhite: "#ffffff",
    },
  },
};

interface Project {
  name: string;
  pool: string | null;
  terminal: { theme: string | null; icon: string | null; title: string | null };
}
const projects = await invoke<Project[]>("list_projects");
const cfg = projects.find((p) => p.name === project);
if (cfg?.pool) document.getElementById("pool")!.textContent = cfg.pool;
document.getElementById("project-name")!.textContent =
  cfg?.terminal.title ?? project;

const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
const theme = picked.xterm;

// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
document.documentElement.style.background = theme.background;
document.body.style.background = theme.background;
const header = document.querySelector("header") as HTMLElement;
header.style.background = picked.header;
header.style.borderBottomColor = picked.border;
header.style.color = theme.foreground;

const term = new Terminal({
  // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
  allowProposedApi: true,
  fontFamily: '"JetBrains Mono", Menlo, monospace',
  fontSize: 13,
  fontWeightBold: "600",
  lineHeight: 1.25,
  letterSpacing: 0,
  cursorBlink: true,
  cursorStyle: "bar",
  scrollback: 10000,
  theme,
});
const fit = new FitAddon();
term.loadAddon(fit);

// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
term.attachCustomKeyEventHandler((e) => {
  if (e.key === "Enter" && e.shiftKey) {
    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
    return false;
  }
  return true;
});

// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
await document.fonts.load('13px "JetBrains Mono"');
term.open(document.getElementById("term")!);
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
term.loadAddon(new LigaturesAddon());
term.loadAddon(new WebglAddon());
fit.fit();

function b64ToBytes(b64: string): Uint8Array {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));

term.onData((data) => invoke("term_write", { data }));
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
window.addEventListener("resize", () => fit.fit());

await invoke("term_start", { project, rows: term.rows, cols: term.cols });
term.focus();


1	use base64::{engine::general_purpose::STANDARD, Engine};
2	use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, MasterPty, PtySize};
3	use std::collections::HashMap;
4	use std::io::{Read, Write};
5	use std::path::PathBuf;
6	use std::sync::Mutex;
7	use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
8
9	/// Eine laufende PTY-Session, gekoppelt an ein Terminal-Fenster (Key = Fenster-Label).
10	pub struct Session {
11	  writer: Box<dyn Write + Send>,
12	  master: Box<dyn MasterPty + Send>,
13	  killer: Box<dyn ChildKiller + Send + Sync>,
14	}
15
16	#[derive(Default)]
17	pub struct Terminals(pub Mutex<HashMap<String, Session>>);
18
19	/// Öffnet ein Terminal für ein Projekt: startet das eigene Binary als
20	/// Terminal-Prozess (`--terminal <projekt>`), damit jedes Terminal ein
21	/// eigenes Dock-Icon bekommt.
22	#[tauri::command]
23	pub fn open_terminal(project: String) -> Result<(), String> {
24	  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
25	  let dir = PathBuf::from(home).join("claude-projects").join(&project);
26	  if !dir.is_dir() {
27	    return Err(format!("Projektordner fehlt: {}", dir.display()));
28	  }
29	  let exe = std::env::current_exe().map_err(|e| e.to_string())?;
30	  std::process::Command::new(exe)
31	    .args(["--terminal", &project])
32	    .spawn()
33	    .map_err(|e| e.to_string())?;
34	  Ok(())
35	}
36
37	/// Fenster-Hintergrund je Theme — muss zu den Theme-Definitionen in
38	/// terminal.ts passen, sonst blitzt beim Öffnen die falsche Farbe auf.
39	fn theme_background(theme: &str) -> (u8, u8, u8) {
40	  match theme {
41	    "dracula" => (0x28, 0x2a, 0x36),
42	    "solarized-dark" => (0x00, 0x2b, 0x36),
43	    "gruvbox" => (0x28, 0x28, 0x28),
44	    "one-dark" => (0x28, 0x2c, 0x34),
45	    _ => (0x1e, 0x1e, 0x2e), // Catppuccin Mocha
46	  }
47	}
48
49	/// Setzt das Dock-Icon dieses Terminal-Prozesses aus einer PNG/ICNS-Datei.
50	#[cfg(target_os = "macos")]
51	pub fn set_dock_icon(path: &str) {
52	  use objc2::{AnyThread, MainThreadMarker};
53	  use objc2_app_kit::{NSApplication, NSImage};
54	  use objc2_foundation::NSString;
55
56	  let mtm = MainThreadMarker::new().expect("set_dock_icon läuft nicht auf dem Main-Thread");
57	  let img = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path))
58	    .unwrap_or_else(|| panic!("Dock-Icon nicht ladbar: {path}"));
59	  // unsafe laut objc2-Signatur; das NSImage stammt aus einer Datei und ist gültig.
60	  unsafe {
61	    NSApplication::sharedApplication(mtm).setApplicationIconImage(Some(&img));
62	  }
63	}
64
65	/// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst,
66	/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
67	/// Ausgaben verloren, bevor der Event-Listener steht.
68	pub fn build_window(
69	  app: &AppHandle,
70	  project: &str,
71	  cfg: &crate::TerminalConfig,
72	) -> tauri::Result<()> {
73	  let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
74	  let title = cfg.title.as_deref().unwrap_or(project);
75	  let builder = WebviewWindowBuilder::new(
76	    app,
77	    format!("term-{project}"),
78	    WebviewUrl::App(format!("terminal.html?project={project}").into()),
79	  )
80	  .title(format!("{title} — Session"))
81	  .inner_size(980.0, 640.0)
82	  .background_color(tauri::window::Color(r, g, b, 0xff));
83
84	  // Titelbar liegt über dem Inhalt; der Header in terminal.html ist Drag-Region.
85	  #[cfg(target_os = "macos")]
86	  let builder = builder
87	    .title_bar_style(tauri::TitleBarStyle::Overlay)
88	    .hidden_title(true);
89
90	  builder.build()?;
91	  Ok(())
92	}
93
94	/// Startet die PTY für das rufende Fenster: `claude-sync` im Projektordner.
95	/// zsh -i lädt die .zshrc (PATH, fnm); Pool, Sentinel und git-Sync macht
96	/// claude-sync selbst.
97	#[tauri::command]
98	pub fn term_start(
99	  window: tauri::WebviewWindow,
100	  terminals: State<Terminals>,
101	  project: String,
102	  rows: u16,
103	  cols: u16,
104	) -> Result<(), String> {
105	  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
106	  let cwd = PathBuf::from(home).join("claude-projects").join(&project);
107
108	  let pty = native_pty_system()
109	    .openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
110	    .map_err(|e| e.to_string())?;
111
112	  let mut cmd = CommandBuilder::new("/bin/zsh");
113	  cmd.args(["-ic", "claude-sync"]);
114	  cmd.cwd(&cwd);
115	  cmd.env("TERM", "xterm-256color");
116
117	  let mut child = pty.slave.spawn_command(cmd).map_err(|e| e.to_string())?;
118	  drop(pty.slave);
119
120	  let killer = child.clone_killer();
121	  let writer = pty.master.take_writer().map_err(|e| e.to_string())?;
122	  let mut reader = pty.master.try_clone_reader().map_err(|e| e.to_string())?;
123
124	  let label = window.label().to_string();
125	  let app = window.app_handle().clone();
126	  std::thread::spawn(move || {
127	    let mut buf = [0u8; 8192];
128	    loop {
129	      match reader.read(&mut buf) {
130	        Ok(0) | Err(_) => break,
131	        Ok(n) => {
132	          let _ = app.emit_to(&label, "pty-output", STANDARD.encode(&buf[..n]));
133	        }
134	      }
135	    }
136	    let _ = child.wait();
137	    let _ = app.emit_to(&label, "pty-exit", ());
138	  });
139
140	  terminals.0.lock().unwrap().insert(
141	    window.label().to_string(),
142	    Session { writer, master: pty.master, killer },
143	  );
144	  Ok(())
145	}
146
147	/// Debug: JS-Fehler aus dem Terminal-Fenster ins Dev-Log.
148	#[tauri::command]
149	pub fn term_log(window: tauri::WebviewWindow, msg: String) {
150	  eprintln!("[{}] {msg}", window.label());
151	}
152
153	/// Tastatureingaben des Fensters in die PTY.
154	#[tauri::command]
155	pub fn term_write(
156	  window: tauri::WebviewWindow,
157	  terminals: State<Terminals>,
158	  data: String,
159	) -> Result<(), String> {
160	  let mut map = terminals.0.lock().unwrap();
161	  let s = map
162	    .get_mut(window.label())
163	    .ok_or("keine Session für dieses Fenster")?;
164	  s.writer.write_all(data.as_bytes()).map_err(|e| e.to_string())
165	}
166
167	#[tauri::command]
168	pub fn term_resize(
169	  window: tauri::WebviewWindow,
170	  terminals: State<Terminals>,
171	  rows: u16,
172	  cols: u16,
173	) -> Result<(), String> {
174	  let map = terminals.0.lock().unwrap();
175	  let s = map
176	    .get(window.label())
177	    .ok_or("keine Session für dieses Fenster")?;
178	  s.master
179	    .resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
180	    .map_err(|e| e.to_string())
181	}
182
183	/// Beim Schließen eines Terminal-Fensters: Kind killen, Session verwerfen.
184	/// Das Droppen des Masters schließt die PTY — claude bekommt HUP.
185	pub fn close(window: &tauri::Window) {
186	  let terminals = window.state::<Terminals>();
187	  let session = terminals.0.lock().unwrap().remove(window.label());
188	  if let Some(mut s) = session {
189	    let _ = s.killer.kill();
190	  }
191	}
192

use base64::{engine::general_purpose::STANDARD, Engine};
use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, MasterPty, PtySize};
use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};

/// Eine laufende PTY-Session, gekoppelt an ein Terminal-Fenster (Key = Fenster-Label).
pub struct Session {
  writer: Box<dyn Write + Send>,
  master: Box<dyn MasterPty + Send>,
  killer: Box<dyn ChildKiller + Send + Sync>,
}

#[derive(Default)]
pub struct Terminals(pub Mutex<HashMap<String, Session>>);

/// Öffnet ein Terminal für ein Projekt: startet das eigene Binary als
/// Terminal-Prozess (`--terminal <projekt>`), damit jedes Terminal ein
/// eigenes Dock-Icon bekommt.
#[tauri::command]
pub fn open_terminal(project: String) -> Result<(), String> {
  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
  let dir = PathBuf::from(home).join("claude-projects").join(&project);
  if !dir.is_dir() {
    return Err(format!("Projektordner fehlt: {}", dir.display()));
  }
  let exe = std::env::current_exe().map_err(|e| e.to_string())?;
  std::process::Command::new(exe)
    .args(["--terminal", &project])
    .spawn()
    .map_err(|e| e.to_string())?;
  Ok(())
}

/// Fenster-Hintergrund je Theme — muss zu den Theme-Definitionen in
/// terminal.ts passen, sonst blitzt beim Öffnen die falsche Farbe auf.
fn theme_background(theme: &str) -> (u8, u8, u8) {
  match theme {
    "dracula" => (0x28, 0x2a, 0x36),
    "solarized-dark" => (0x00, 0x2b, 0x36),
    "gruvbox" => (0x28, 0x28, 0x28),
    "one-dark" => (0x28, 0x2c, 0x34),
    _ => (0x1e, 0x1e, 0x2e), // Catppuccin Mocha
  }
}

/// Setzt das Dock-Icon dieses Terminal-Prozesses aus einer PNG/ICNS-Datei.
#[cfg(target_os = "macos")]
pub fn set_dock_icon(path: &str) {
  use objc2::{AnyThread, MainThreadMarker};
  use objc2_app_kit::{NSApplication, NSImage};
  use objc2_foundation::NSString;

  let mtm = MainThreadMarker::new().expect("set_dock_icon läuft nicht auf dem Main-Thread");
  let img = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path))
    .unwrap_or_else(|| panic!("Dock-Icon nicht ladbar: {path}"));
  // unsafe laut objc2-Signatur; das NSImage stammt aus einer Datei und ist gültig.
  unsafe {
    NSApplication::sharedApplication(mtm).setApplicationIconImage(Some(&img));
  }
}

/// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst,
/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
/// Ausgaben verloren, bevor der Event-Listener steht.
pub fn build_window(
  app: &AppHandle,
  project: &str,
  cfg: &crate::TerminalConfig,
) -> tauri::Result<()> {
  let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
  let title = cfg.title.as_deref().unwrap_or(project);
  let builder = WebviewWindowBuilder::new(
    app,
    format!("term-{project}"),
    WebviewUrl::App(format!("terminal.html?project={project}").into()),
  )
  .title(format!("{title} — Session"))
  .inner_size(980.0, 640.0)
  .background_color(tauri::window::Color(r, g, b, 0xff));

  // Titelbar liegt über dem Inhalt; der Header in terminal.html ist Drag-Region.
  #[cfg(target_os = "macos")]
  let builder = builder
    .title_bar_style(tauri::TitleBarStyle::Overlay)
    .hidden_title(true);

  builder.build()?;
  Ok(())
}

/// Startet die PTY für das rufende Fenster: `claude-sync` im Projektordner.
/// zsh -i lädt die .zshrc (PATH, fnm); Pool, Sentinel und git-Sync macht
/// claude-sync selbst.
#[tauri::command]
pub fn term_start(
  window: tauri::WebviewWindow,
  terminals: State<Terminals>,
  project: String,
  rows: u16,
  cols: u16,
) -> Result<(), String> {
  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
  let cwd = PathBuf::from(home).join("claude-projects").join(&project);

  let pty = native_pty_system()
    .openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
    .map_err(|e| e.to_string())?;

  let mut cmd = CommandBuilder::new("/bin/zsh");
  cmd.args(["-ic", "claude-sync"]);
  cmd.cwd(&cwd);
  cmd.env("TERM", "xterm-256color");

  let mut child = pty.slave.spawn_command(cmd).map_err(|e| e.to_string())?;
  drop(pty.slave);

  let killer = child.clone_killer();
  let writer = pty.master.take_writer().map_err(|e| e.to_string())?;
  let mut reader = pty.master.try_clone_reader().map_err(|e| e.to_string())?;

  let label = window.label().to_string();
  let app = window.app_handle().clone();
  std::thread::spawn(move || {
    let mut buf = [0u8; 8192];
    loop {
      match reader.read(&mut buf) {
        Ok(0) | Err(_) => break,
        Ok(n) => {
          let _ = app.emit_to(&label, "pty-output", STANDARD.encode(&buf[..n]));
        }
      }
    }
    let _ = child.wait();
    let _ = app.emit_to(&label, "pty-exit", ());
  });

  terminals.0.lock().unwrap().insert(
    window.label().to_string(),
    Session { writer, master: pty.master, killer },
  );
  Ok(())
}

/// Debug: JS-Fehler aus dem Terminal-Fenster ins Dev-Log.
#[tauri::command]
pub fn term_log(window: tauri::WebviewWindow, msg: String) {
  eprintln!("[{}] {msg}", window.label());
}

/// Tastatureingaben des Fensters in die PTY.
#[tauri::command]
pub fn term_write(
  window: tauri::WebviewWindow,
  terminals: State<Terminals>,
  data: String,
) -> Result<(), String> {
  let mut map = terminals.0.lock().unwrap();
  let s = map
    .get_mut(window.label())
    .ok_or("keine Session für dieses Fenster")?;
  s.writer.write_all(data.as_bytes()).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn term_resize(
  window: tauri::WebviewWindow,
  terminals: State<Terminals>,
  rows: u16,
  cols: u16,
) -> Result<(), String> {
  let map = terminals.0.lock().unwrap();
  let s = map
    .get(window.label())
    .ok_or("keine Session für dieses Fenster")?;
  s.master
    .resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
    .map_err(|e| e.to_string())
}

/// Beim Schließen eines Terminal-Fensters: Kind killen, Session verwerfen.
/// Das Droppen des Masters schließt die PTY — claude bekommt HUP.
pub fn close(window: &tauri::Window) {
  let terminals = window.state::<Terminals>();
  let session = terminals.0.lock().unwrap().remove(window.label());
  if let Some(mut s) = session {
    let _ = s.killer.kill();
  }
}


1	<!doctype html>
2
3
4	    <meta charset="UTF-8" />
5	    <title>Terminal</title>
6
64
65
66
67
68
69
70

71
72
73
74

<!doctype html>

    <meta charset="UTF-8" />
    <title>Terminal</title>


1	{
2	  "name": "ai-control",
3	  "private": true,
4	  "version": "0.0.0",
5	  "type": "module",
6	  "scripts": {
7	    "dev": "vite",
8	    "build": "vue-tsc -b && vite build",
9	    "preview": "vite preview",
10	    "tauri": "tauri"
11	  },
12	  "dependencies": {
13	    "@fontsource/jetbrains-mono": "^5.2.8",
14	    "@tauri-apps/api": "^2.11.1",
15	    "@tauri-apps/plugin-autostart": "^2.5.1",
16	    "@tauri-apps/plugin-dialog": "^2.7.1",
17	    "@tauri-apps/plugin-opener": "^2.5.4",
18	    "@xterm/addon-fit": "^0.11.0",
19	    "@xterm/addon-ligatures": "^0.10.0",
20	    "@xterm/addon-webgl": "^0.19.0",
21	    "@xterm/xterm": "^6.0.0",
22	    "vue": "^3.5.39",
23	    "vue-i18n": "^11.4.6"
24	  },
25	  "devDependencies": {
26	    "@tauri-apps/cli": "^2.11.4",
27	    "@types/node": "^24.13.2",
28	    "@vitejs/plugin-vue": "^6.0.7",
29	    "@vue/tsconfig": "^0.9.1",
30	    "typescript": "~6.0.2",
31	    "vite": "^8.1.1",
32	    "vue-tsc": "^3.3.5"
33	  }
34	}
35


Continue from where you left off.


No response requested.


alles drinn?


- 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


Nein. Stand im Code:

- **WebGL + Ligaturen:** drin (`terminal.ts:227-228`, Reihenfolge korrekt: Ligaturen vor WebGL).
- **Font-Metrik:** größtenteils drin — `document.fonts.load('13px "JetBrains Mono"')` vor `term.open()`, `fontSize: 13`, `letterSpacing: 0`. Zwei Abweichungen: `lineHeight: 1.25` (nicht 1.0), und vorgeladen wird nur Gewicht 400 — `fontWeightBold: "600"` wird nicht per `fonts.load` vorgeladen.
- **Output-Batching:** fehlt komplett. `terminal.rs:126-138` emittiert weiterhin jeden PTY-Read (bis 8 KB) sofort als einzelnes base64-Event — kein Sammeln, kein Flush-Intervall.


ok -- brings zu ende


Edit

import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/700.css";

import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "@fontsource/jetbrains-mono/700.css";


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


  fontSize: 13,
  fontWeightBold: "600",
  lineHeight: 1.25,

  fontSize: 13,
  fontWeightBold: "600",
  lineHeight: 1.0,


// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
await document.fonts.load('13px "JetBrains Mono"');

// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut —
// beide verwendeten Gewichte (400 normal, 600 bold).
await document.fonts.load('13px "JetBrains Mono"');
await document.fonts.load('600 13px "JetBrains Mono"');


1	import { Terminal } from "@xterm/xterm";
2	import { FitAddon } from "@xterm/addon-fit";
3	import { WebglAddon } from "@xterm/addon-webgl";
4	import { LigaturesAddon } from "@xterm/addon-ligatures";
5	import "@xterm/xterm/css/xterm.css";
6	import "@fontsource/jetbrains-mono/400.css";
7	import "@fontsource/jetbrains-mono/500.css";
8	import "@fontsource/jetbrains-mono/600.css";
9	import "@fontsource/jetbrains-mono/700.css";
10	import { invoke } from "@tauri-apps/api/core";
11	import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
12
13	// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
14	function showError(msg: string) {
15	  const el = document.createElement("pre");
16	  el.style.cssText =
17	    "color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
18	  el.textContent = msg;
19	  document.body.appendChild(el);
20	  invoke("term_log", { msg });
21	}
22	window.addEventListener("error", (e) =>
23	  showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
24	);
25	window.addEventListener("unhandledrejection", (e) =>
26	  showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
27	);
28
29	const project = new URLSearchParams(location.search).get("project")!;
30	const win = getCurrentWebviewWindow();

import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebglAddon } from "@xterm/addon-webgl";
import { LigaturesAddon } from "@xterm/addon-ligatures";
import "@xterm/xterm/css/xterm.css";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "@fontsource/jetbrains-mono/700.css";
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
function showError(msg: string) {
  const el = document.createElement("pre");
  el.style.cssText =
    "color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
  el.textContent = msg;
  document.body.appendChild(el);
  invoke("term_log", { msg });
}
window.addEventListener("error", (e) =>
  showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
);
window.addEventListener("unhandledrejection", (e) =>
  showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
);

const project = new URLSearchParams(location.search).get("project")!;
const win = getCurrentWebviewWindow();


190	document.documentElement.style.background = theme.background;
191	document.body.style.background = theme.background;
192	const header = document.querySelector("header") as HTMLElement;
193	header.style.background = picked.header;
194	header.style.borderBottomColor = picked.border;
195	header.style.color = theme.foreground;
196
197	const term = new Terminal({
198	  // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
199	  allowProposedApi: true,
200	  fontFamily: '"JetBrains Mono", Menlo, monospace',
201	  fontSize: 13,
202	  fontWeightBold: "600",
203	  lineHeight: 1.25,
204	  letterSpacing: 0,
205	  cursorBlink: true,
206	  cursorStyle: "bar",
207	  scrollback: 10000,
208	  theme,
209	});
210	const fit = new FitAddon();
211	term.loadAddon(fit);
212
213	// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
214	// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
215	// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
216	term.attachCustomKeyEventHandler((e) => {
217	  if (e.key === "Enter" && e.shiftKey) {
218	    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
219	    return false;
220	  }
221	  return true;
222	});
223
224	// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
225	await document.fonts.load('13px "JetBrains Mono"');
226	term.open(document.getElementById("term")!);
227	// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
228	term.loadAddon(new LigaturesAddon());
229	term.loadAddon(new WebglAddon());
230	fit.fit();
231
232	function b64ToBytes(b64: string): Uint8Array {
233	  const bin = atob(b64);
234	  const bytes = new Uint8Array(bin.length);
235	  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
236	  return bytes;
237	}
238
239	await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
240	await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));
241
242	term.onData((data) => invoke("term_write", { data }));
243	term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
244	window.addEventListener("resize", () => fit.fit());
245
246	await invoke("term_start", { project, rows: term.rows, cols: term.cols });
247	term.focus();
248

document.documentElement.style.background = theme.background;
document.body.style.background = theme.background;
const header = document.querySelector("header") as HTMLElement;
header.style.background = picked.header;
header.style.borderBottomColor = picked.border;
header.style.color = theme.foreground;

const term = new Terminal({
  // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
  allowProposedApi: true,
  fontFamily: '"JetBrains Mono", Menlo, monospace',
  fontSize: 13,
  fontWeightBold: "600",
  lineHeight: 1.25,
  letterSpacing: 0,
  cursorBlink: true,
  cursorStyle: "bar",
  scrollback: 10000,
  theme,
});
const fit = new FitAddon();
term.loadAddon(fit);

// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
term.attachCustomKeyEventHandler((e) => {
  if (e.key === "Enter" && e.shiftKey) {
    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
    return false;
  }
  return true;
});

// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
await document.fonts.load('13px "JetBrains Mono"');
term.open(document.getElementById("term")!);
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
term.loadAddon(new LigaturesAddon());
term.loadAddon(new WebglAddon());
fit.fit();

function b64ToBytes(b64: string): Uint8Array {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));

term.onData((data) => invoke("term_write", { data }));
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
window.addEventListener("resize", () => fit.fit());

await invoke("term_start", { project, rows: term.rows, cols: term.cols });
term.focus();


The file /Users/marcus.hinz/projects/ai-control/src/terminal.ts has been updated successfully. (file state is current in your context — no need to Read it back)

import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebglAddon } from "@xterm/addon-webgl";
import { LigaturesAddon } from "@xterm/addon-ligatures";
import "@xterm/xterm/css/xterm.css";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "@fontsource/jetbrains-mono/700.css";
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";

// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
function showError(msg: string) {
  const el = document.createElement("pre");
  el.style.cssText =
    "color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
  el.textContent = msg;
  document.body.appendChild(el);
  invoke("term_log", { msg });
}
window.addEventListener("error", (e) =>
  showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
);
window.addEventListener("unhandledrejection", (e) =>
  showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
);

const project = new URLSearchParams(location.search).get("project")!;
const win = getCurrentWebviewWindow();

// Fensterhintergründe je Theme stehen zusätzlich in terminal.rs
// (theme_background) — beide Stellen müssen zusammenpassen.
const THEMES: Record<
  string,
  { header: string; border: string; xterm: Record<string, string> }
> = {
  mocha: {
    header: "#181825",
    border: "#313244",
    xterm: {
      background: "#1e1e2e",
      foreground: "#cdd6f4",
      cursor: "#f5e0dc",
      cursorAccent: "#1e1e2e",
      selectionBackground: "#585b7080",
      black: "#45475a",
      red: "#f38ba8",
      green: "#a6e3a1",
      yellow: "#f9e2af",
      blue: "#89b4fa",
      magenta: "#f5c2e7",
      cyan: "#94e2d5",
      white: "#bac2de",
      brightBlack: "#585b70",
      brightRed: "#f38ba8",
      brightGreen: "#a6e3a1",
      brightYellow: "#f9e2af",
      brightBlue: "#89b4fa",
      brightMagenta: "#f5c2e7",
      brightCyan: "#94e2d5",
      brightWhite: "#a6adc8",
    },
  },
  dracula: {
    header: "#21222c",
    border: "#44475a",
    xterm: {
      background: "#282a36",
      foreground: "#f8f8f2",
      cursor: "#f8f8f2",
      cursorAccent: "#282a36",
      selectionBackground: "#44475a80",
      black: "#21222c",
      red: "#ff5555",
      green: "#50fa7b",
      yellow: "#f1fa8c",
      blue: "#bd93f9",
      magenta: "#ff79c6",
      cyan: "#8be9fd",
      white: "#f8f8f2",
      brightBlack: "#6272a4",
      brightRed: "#ff6e6e",
      brightGreen: "#69ff94",
      brightYellow: "#ffffa5",
      brightBlue: "#d6acff",
      brightMagenta: "#ff92df",
      brightCyan: "#a4ffff",
      brightWhite: "#ffffff",
    },
  },
  "solarized-dark": {
    header: "#073642",
    border: "#586e75",
    xterm: {
      background: "#002b36",
      foreground: "#839496",
      cursor: "#839496",
      cursorAccent: "#002b36",
      selectionBackground: "#07364280",
      black: "#073642",
      red: "#dc322f",
      green: "#859900",
      yellow: "#b58900",
      blue: "#268bd2",
      magenta: "#d33682",
      cyan: "#2aa198",
      white: "#eee8d5",
      brightBlack: "#586e75",
      brightRed: "#cb4b16",
      brightGreen: "#859900",
      brightYellow: "#657b83",
      brightBlue: "#839496",
      brightMagenta: "#6c71c4",
      brightCyan: "#93a1a1",
      brightWhite: "#fdf6e3",
    },
  },
  gruvbox: {
    header: "#1d2021",
    border: "#3c3836",
    xterm: {
      background: "#282828",
      foreground: "#ebdbb2",
      cursor: "#ebdbb2",
      cursorAccent: "#282828",
      selectionBackground: "#3c383680",
      black: "#282828",
      red: "#cc241d",
      green: "#98971a",
      yellow: "#d79921",
      blue: "#458588",
      magenta: "#b16286",
      cyan: "#689d6a",
      white: "#a89984",
      brightBlack: "#928374",
      brightRed: "#fb4934",
      brightGreen: "#b8bb26",
      brightYellow: "#fabd2f",
      brightBlue: "#83a598",
      brightMagenta: "#d3869b",
      brightCyan: "#8ec07c",
      brightWhite: "#ebdbb2",
    },
  },
  "one-dark": {
    header: "#21252b",
    border: "#3e4451",
    xterm: {
      background: "#282c34",
      foreground: "#abb2bf",
      cursor: "#abb2bf",
      cursorAccent: "#282c34",
      selectionBackground: "#3e445180",
      black: "#282c34",
      red: "#e06c75",
      green: "#98c379",
      yellow: "#e5c07b",
      blue: "#61afef",
      magenta: "#c678dd",
      cyan: "#56b6c2",
      white: "#abb2bf",
      brightBlack: "#5c6370",
      brightRed: "#e06c75",
      brightGreen: "#98c379",
      brightYellow: "#e5c07b",
      brightBlue: "#61afef",
      brightMagenta: "#c678dd",
      brightCyan: "#56b6c2",
      brightWhite: "#ffffff",
    },
  },
};

interface Project {
  name: string;
  pool: string | null;
  terminal: { theme: string | null; icon: string | null; title: string | null };
}
const projects = await invoke<Project[]>("list_projects");
const cfg = projects.find((p) => p.name === project);
if (cfg?.pool) document.getElementById("pool")!.textContent = cfg.pool;
document.getElementById("project-name")!.textContent =
  cfg?.terminal.title ?? project;

const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
const theme = picked.xterm;

// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
document.documentElement.style.background = theme.background;
document.body.style.background = theme.background;
const header = document.querySelector("header") as HTMLElement;
header.style.background = picked.header;
header.style.borderBottomColor = picked.border;
header.style.color = theme.foreground;

const term = new Terminal({
  // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
  allowProposedApi: true,
  fontFamily: '"JetBrains Mono", Menlo, monospace',
  fontSize: 13,
  fontWeightBold: "600",
  lineHeight: 1.25,
  letterSpacing: 0,
  cursorBlink: true,
  cursorStyle: "bar",
  scrollback: 10000,
  theme,
});
const fit = new FitAddon();
term.loadAddon(fit);

// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
term.attachCustomKeyEventHandler((e) => {
  if (e.key === "Enter" && e.shiftKey) {
    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
    return false;
  }
  return true;
});

// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
await document.fonts.load('13px "JetBrains Mono"');
term.open(document.getElementById("term")!);
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
term.loadAddon(new LigaturesAddon());
term.loadAddon(new WebglAddon());
fit.fit();

function b64ToBytes(b64: string): Uint8Array {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));

term.onData((data) => invoke("term_write", { data }));
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
window.addEventListener("resize", () => fit.fit());

await invoke("term_start", { project, rows: term.rows, cols: term.cols });
term.focus();


// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
await document.fonts.load('13px "JetBrains Mono"');

// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut —
// beide verwendeten Gewichte (400 normal, 600 bold).
await document.fonts.load('13px "JetBrains Mono"');
await document.fonts.load('600 13px "JetBrains Mono"');