  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key).unwrap()
  }

  fn make_apikey_pool(p: &Paths, store: &dyn ApikeyStore, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, store, name, key, true).unwrap()
  }

-    create_apikey_pool_in(p, store, name, key).unwrap()

+    create_apikey_pool_in(p, store, name, key, true).unwrap()


Edit

let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2").unwrap_err();

let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2", true).unwrap_err();


The file /home/marcuh/projects/ai-control/src-tauri/src/lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

make_apikey_pool(&p, &store, "kunde", "sk-1");

-    let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2").unwrap_err();

+    let err = create_apikey_pool_in(&p, &store, "kunde", "sk-2", true).unwrap_err();


assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ").is_err());

assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ", true).is_err());


-    assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ").is_err());

+    assert!(create_apikey_pool_in(&p, &map_store(), "kunde", "  ", true).is_err());


assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1").is_err());

assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1", true).is_err());


-    assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1").is_err());

+    assert!(create_apikey_pool_in(&p, &map_store(), "a/b", "sk-1", true).is_err());


    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu", false).unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");


    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

    let id = make_apikey_pool(&p, &store, "kunde", "sk-alt");
    set_apikey_in(&p, &store, &id, "sk-neu", false).unwrap();
    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

-    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

+    set_apikey_in(&p, &store, &id, "sk-neu", false).unwrap();


    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu", false).unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");


    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu").unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");

    let store = map_store();
    set_apikey_in(&p, &store, &id, "sk-neu", false).unwrap();

    assert_eq!(store.0.lock().unwrap().get(&id).unwrap(), "sk-neu");


    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);

    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu", true).unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);


    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);

    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    set_apikey_in(&p, &FailStore, &id, "sk-neu", true).unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);

-    set_apikey_in(&p, &FailStore, &id, "sk-neu").unwrap();

+    set_apikey_in(&p, &FailStore, &id, "sk-neu", true).unwrap();


    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());

    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1", true).is_err());


    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());

    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1", true).is_err());

-    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1").is_err());

+    assert!(set_apikey_in(&p, &map_store(), &id, "sk-1", true).is_err());


Benenne Pool privateDefault in private um


  #[test]
  fn apikey_helper_kette_referenz() {

  /// Ohne Store und ohne allow_file bricht die Anlage ab — es entsteht nichts.
  #[test]
  fn apikey_anlegen_abbruch_ohne_store() {
    let p = tmp_paths();
    let err = create_apikey_pool_in(&p, &FailStore, "kunde", "sk-1", false).unwrap_err();
    assert_eq!(err, KEYCHAIN_UNAVAILABLE);
    assert!(pool_names(&p).unwrap().is_empty());
    assert!(!p.pools_dir().exists());
  }

  /// Key-Ändern ohne Store und ohne allow_file lässt alles unangetastet.
  #[test]
  fn apikey_aendern_abbruch_ohne_store() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    let dir = p.pool_dir(&id);
    let settings_before = fs::read_to_string(dir.join("settings.json")).unwrap();

    let err = set_apikey_in(&p, &FailStore, &id, "sk-neu", false).unwrap_err();
    assert_eq!(err, KEYCHAIN_UNAVAILABLE);
    assert_eq!(fs::read_to_string(dir.join(APIKEY_FILE)).unwrap(), "sk-alt\n");
    assert_eq!(fs::read_to_string(dir.join("settings.json")).unwrap(), settings_before);
  }

  #[test]
  fn apikey_helper_kette_referenz() {


  /// Ohne Store und ohne allow_file bricht die Anlage ab — es entsteht nichts.
  #[test]
  fn apikey_anlegen_abbruch_ohne_store() {
    let p = tmp_paths();
    let err = create_apikey_pool_in(&p, &FailStore, "kunde", "sk-1", false).unwrap_err();
    assert_eq!(err, KEYCHAIN_UNAVAILABLE);
    assert!(pool_names(&p).unwrap().is_empty());
    assert!(!p.pools_dir().exists());
  }

  /// Key-Ändern ohne Store und ohne allow_file lässt alles unangetastet.
  #[test]
  fn apikey_aendern_abbruch_ohne_store() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");
    let dir = p.pool_dir(&id);
    let settings_before = fs::read_to_string(dir.join("settings.json")).unwrap();

    let err = set_apikey_in(&p, &FailStore, &id, "sk-neu", false).unwrap_err();
    assert_eq!(err, KEYCHAIN_UNAVAILABLE);
    assert_eq!(fs::read_to_string(dir.join(APIKEY_FILE)).unwrap(), "sk-alt\n");
    assert_eq!(fs::read_to_string(dir.join("settings.json")).unwrap(), settings_before);
  }

  #[test]
  fn apikey_helper_kette_referenz() {

+  /// Ohne Store und ohne allow_file bricht die Anlage ab — es entsteht nichts.

+  fn apikey_anlegen_abbruch_ohne_store() {

+    let err = create_apikey_pool_in(&p, &FailStore, "kunde", "sk-1", false).unwrap_err();

+    assert_eq!(err, KEYCHAIN_UNAVAILABLE);

+    assert!(pool_names(&p).unwrap().is_empty());

+  /// Key-Ändern ohne Store und ohne allow_file lässt alles unangetastet.

+  fn apikey_aendern_abbruch_ohne_store() {

+    let id = make_apikey_pool(&p, &FailStore, "kunde", "sk-alt");

+    let settings_before = fs::read_to_string(dir.join("settings.json")).unwrap();

+    let err = set_apikey_in(&p, &FailStore, &id, "sk-neu", false).unwrap_err();

+    assert_eq!(fs::read_to_string(dir.join(APIKEY_FILE)).unwrap(), "sk-alt\n");

+    assert_eq!(fs::read_to_string(dir.join("settings.json")).unwrap(), settings_before);

let cmd = apikey_helper_command(&dir, "abc");


Bash

CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep -E "^error|test result|panicked" | head

Rust-Tests laufen lassen


test result: ok. 48 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


48 Tests grün. Jetzt die UI:


Edit

async function submit() {
  const d = dialog.value;
  if (!d) return;
  busy.value = true;
  error.value = "";
  try {
    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });
    } else {
      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value });
    }
    dialog.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

// Zweistufig: erster Versuch ohne Datei-Erlaubnis; meldet das Backend
// keychain-unavailable, fragt ein Dialog nach und wiederholt mit allowFile.
const fileConfirm = ref(false);

async function submit(allowFile = false) {
  const d = dialog.value;
  if (!d) return;
  busy.value = true;
  error.value = "";
  try {
    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value, allowFile });
    } else {
      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value, allowFile });
    }
    dialog.value = null;
    await refresh();
  } catch (e) {
    if (String(e) === "keychain-unavailable") {
      fileConfirm.value = true;
    } else {
      error.value = String(e);
    }
  } finally {
    busy.value = false;
  }
}

async function confirmFileFallback() {
  fileConfirm.value = false;
  await submit(true);
}


The file /home/marcuh/projects/ai-control/src/components/PoolList.vue has been updated successfully. (file state is current in your context — no need to Read it back)

async function submit() {
  const d = dialog.value;
  if (!d) return;
  busy.value = true;
  error.value = "";
  try {
    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });
    } else {
      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value });
    }
    dialog.value = null;
    await refresh();
  } catch (e) {
    error.value = String(e);
  } finally {
    busy.value = false;
  }
}

// Zweistufig: erster Versuch ohne Datei-Erlaubnis; meldet das Backend
// keychain-unavailable, fragt ein Dialog nach und wiederholt mit allowFile.
const fileConfirm = ref(false);

async function submit(allowFile = false) {
  const d = dialog.value;
  if (!d) return;
  busy.value = true;
  error.value = "";
  try {
    if (d.mode === "oauth") {
      await invoke("create_oauth_pool", { name: dName.value });
    } else if (d.editing) {
      await invoke("set_apikey", { pool: dPool.value, key: dKey.value, allowFile });
    } else {
      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value, allowFile });
    }
    dialog.value = null;
    await refresh();
  } catch (e) {
    if (String(e) === "keychain-unavailable") {
      fileConfirm.value = true;
    } else {
      error.value = String(e);
    }
  } finally {
    busy.value = false;
  }
}

async function confirmFileFallback() {
  fileConfirm.value = false;
  await submit(true);
}

+// Zweistufig: erster Versuch ohne Datei-Erlaubnis; meldet das Backend

+// keychain-unavailable, fragt ein Dialog nach und wiederholt mit allowFile.

+async function submit(allowFile = false) {

-      await invoke("set_apikey", { pool: dPool.value, key: dKey.value });

+      await invoke("set_apikey", { pool: dPool.value, key: dKey.value, allowFile });

-      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value });

+      await invoke("create_apikey_pool", { name: dName.value, key: dKey.value, allowFile });

+    if (String(e) === "keychain-unavailable") {

const deletePool = ref<Pool | null>(null);


    <form class="dialog" @submit.prevent="submit">
      <h3>{{ dialogTitle }}</h3>

    <form class="dialog" @submit.prevent="submit()">
      <h3>{{ dialogTitle }}</h3>


    <form class="dialog" @submit.prevent="submit">
      <h3>{{ dialogTitle }}</h3>

    <form class="dialog" @submit.prevent="submit()">
      <h3>{{ dialogTitle }}</h3>

<p v-else class="empty">{{ $t("pools.empty") }}</p>

<div v-if="dialog" class="overlay" @click.self="closeDialog">

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

+    <form class="dialog" @submit.prevent="submit()">


<div v-if="renamePool" class="overlay" @click.self="busy ? undefined : (renamePool = null)">

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

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

        <button type="button" :disabled="busy" @click="fileConfirm = false">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmFileFallback">
          {{ $t("pools.storeAsFile") }}
        </button>


<div v-if="renamePool" class="overlay" @click.self="busy ? undefined : (renamePool = null)">

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

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

        <button type="button" :disabled="busy" @click="fileConfirm = false">
          {{ $t("pools.cancel") }}
        </button>
        <button class="danger" :disabled="busy" @click="confirmFileFallback">
          {{ $t("pools.storeAsFile") }}
        </button>

+  <div v-if="fileConfirm" class="overlay">

+      <h3>{{ $t("pools.keychainUnavailableTitle") }}</h3>

+        <button type="button" :disabled="busy" @click="fileConfirm = false">

+        <button class="danger" :disabled="busy" @click="confirmFileFallback">

+          {{ $t("pools.storeAsFile") }}

{{ $t("pools.renameTitle", { name: renamePool.name }) }}


    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
  },
};

    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
    keychainUnavailableTitle: "Keine sichere Ablage",
    keychainUnavailable:
      "Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",
    storeAsFile: "Als Datei ablegen",
  },
};


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

    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
    keychainUnavailableTitle: "Keine sichere Ablage",
    keychainUnavailable:
      "Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",
    storeAsFile: "Als Datei ablegen",
  },
};

import { createI18n } from "vue-i18n";

const de = {
  app: {
    projects: "Projekte",
    pools: "Pools",
    usage: "Verbrauch",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 Tage",
    days30: "30 Tage",
    pool: "Pool",
    project: "Projekt",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ Kosten",
    total: "Summe",
    empty: "Keine Verbrauchsdaten im Zeitraum.",
    estimateNote:
      "API-Gegenwert aus den lokalen Transcripts — keine Abrechnung, Historie nur solange claude sie vorhält.",
  },
  projects: {
    project: "Projekt",
    pool: "Pool",
    noPool: "– kein Pool –",
    start: "Starten",
    stop: "Beenden",
    terminalSettings: "Terminal-Einstellungen",
    stillRunning: "{name} läuft noch",
    poolChangeSaved:
      "Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
    restartHint:
      "Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
    keepRunning: "Weiterlaufen lassen",
    restartNow: "Jetzt neu starten",
    restarting: "Starte neu …",
    terminal: "Terminal — {name}",
    title: "Titel",
    theme: "Theme",
    dockIcon: "Dock-Icon",
    chooseFile: "Datei wählen …",
    remove: "Entfernen",
    defaultIcon: "Standard-Icon",
    appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
    cancel: "Abbrechen",
    save: "Speichern",
    noPoolAssigned: "kein Pool",
    needsPool: "Kein Pool zugeordnet",
    needsKey: "Pool hat keinen API-Key",
    newProject: "+ Projekt",
    wizardTitle: "Neues Projekt",
    name: "Name",
    workDir: "Arbeitsordner",
    workDirNone: "keiner",
    workDirCustom: "vorhandenen Ordner verknüpfen",
    chooseFolder: "Ordner wählen …",
    create: "Anlegen",
    wizardHint:
      "Angelegt werden Projektordner mit memory/, Sentinel-.gitignore, settings.json (Memory-Verzeichnis + Berechtigungen) und die Pool-/Terminal-Zuordnung.",
    delete: "Projekt löschen",
    deleteRunning: "Läuft noch — erst beenden",
    deleteTitle: "{name} löschen",
    deleteWarning:
      "Der Projektordner unter ~/claude-projects (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
    deleteWorkDirs: "Arbeitsordner mitlöschen:",
    workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
    todo: "Todoliste",
    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
  },
  pools: {
    pool: "Pool",
    type: "Typ",
    actions: "Aktionen",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Zurücksetzen",
    rename: "Umbenennen",
    renameTitle: "{name} umbenennen",
    changeKey: "Key ändern",
    insertKey: "Key eintragen",
    delete: "Löschen",
    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
    empty: "Noch keine Pools angelegt.",
    newOauthPool: "Neuen oAuth-Pool anlegen",
    newApikeyPool: "Neuen API-Key-Pool anlegen",
    editKey: "API-Key ändern – {name}",
    name: "Name",
    apiKey: "API-Key",
    oauthHint:
      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
    cancel: "Abbrechen",
    createPool: "Anlegen",
    reset: "Zurücksetzen",
    save: "Speichern",
    deletePool: "Pool löschen",
    deleteWarning:
      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
    deleteBlockedTooltip:
      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
    reloginTitle: "{name} zurücksetzen",
    reloginWarning:
      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
    reloginNoEntry:
      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
  },
};

const en: typeof de = {
  app: {
    projects: "Projects",
    pools: "Pools",
    usage: "Usage",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 days",
    days30: "30 days",
    pool: "Pool",
    project: "Project",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ cost",
    total: "Total",
    empty: "No usage data in this period.",
    estimateNote:
      "API equivalent from local transcripts — not a bill; history only as long as claude keeps it.",
  },
  projects: {
    project: "Project",
    pool: "Pool",
    noPool: "– no pool –",
    start: "Start",
    stop: "Stop",
    terminalSettings: "Terminal settings",
    stillRunning: "{name} is still running",
    poolChangeSaved:
      "The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
    restartHint:
      "Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
    keepRunning: "Keep running",
    restartNow: "Restart now",
    restarting: "Restarting …",
    terminal: "Terminal — {name}",
    title: "Title",
    theme: "Theme",
    dockIcon: "Dock icon",
    chooseFile: "Choose file …",
    remove: "Remove",
    defaultIcon: "Default icon",
    appliesNextStart: "Applies from the next terminal start.",
    cancel: "Cancel",
    save: "Save",
    noPoolAssigned: "no pool",
    needsPool: "No pool assigned",
    needsKey: "Pool has no API key",
    newProject: "+ Project",
    wizardTitle: "New project",
    name: "Name",
    workDir: "Working folder",
    workDirNone: "none",
    workDirCustom: "link existing folder",
    chooseFolder: "Choose folder …",
    create: "Create",
    wizardHint:
      "Creates the project folder with memory/, sentinel .gitignore, settings.json (memory dir + permissions), and the pool/terminal assignment.",
    delete: "Delete project",
    deleteRunning: "Still running — stop it first",
    deleteTitle: "Delete {name}",
    deleteWarning:
      "The project folder under ~/claude-projects (incl. memory/, settings and pool assignment) will be deleted permanently.",
    deleteWorkDirs: "Also delete working folders:",
    workDirsStay: "The working folders stay untouched.",
    todo: "Todo list",
    todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
  },
  pools: {
    pool: "Pool",
    type: "Type",
    actions: "Actions",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Reset",
    rename: "Rename",
    renameTitle: "Rename {name}",
    changeKey: "Change key",
    insertKey: "Insert key",
    delete: "Delete",
    assigned: "no projects | 1 project | {count} projects",
    empty: "No pools yet.",
    newOauthPool: "Create oAuth pool",
    newApikeyPool: "Create API key pool",
    editKey: "Change API key – {name}",
    name: "Name",
    apiKey: "API key",
    oauthHint:
      "The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
    cancel: "Cancel",
    createPool: "Create",
    reset: "Reset",
    save: "Save",
    deletePool: "Delete pool",
    deleteWarning:
      "Pool {name} and its credential file will be deleted. This cannot be undone.",
    deleteUnassigns: "These projects lose their pool assignment:",
    deleteBlockedTooltip:
      "Deletion blocked — active: {projects}. Stop the session first.",
    reloginTitle: "Reset {name}",
    reloginWarning:
      "The access token for {name} stored in the keychain will be deleted. On the next start claude asks for /login again.",
    reloginNoEntry:
      "No keychain entry present — claude signs in via /login on the next start anyway.",
    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

const stored = localStorage.getItem("lang");
const initial =
  stored === "de" || stored === "en"
    ? stored
    : navigator.language.startsWith("de")
      ? "de"
      : "en";

export const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: initial,
  fallbackLocale: "en",
  messages: { de, en },
});

export function setLocale(lang: "de" | "en") {
  i18n.global.locale.value = lang;
  localStorage.setItem("lang", lang);
}

+    keychainUnavailableTitle: "Keine sichere Ablage",

+      "Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",


Bash

grep -n "reloginBlocked" /home/marcuh/projects/ai-control/src/i18n.ts

Englische reloginBlocked-Stelle finden


109:    reloginBlocked:
224:    reloginBlocked:


222	    reloginNoEntry:
223	      "No keychain entry present — claude signs in via /login on the next start anyway.",
224	    reloginBlocked:
225	      "Resetting requires an unused pool. These projects are still running:",
226	  },
227	};
228
229	const stored = localStorage.getItem("lang");

    reloginNoEntry:
      "No keychain entry present — claude signs in via /login on the next start anyway.",
    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

const stored = localStorage.getItem("lang");


Edit

    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
    keychainUnavailableTitle: "No secure storage",
    keychainUnavailable:
      "Keychain/keyring is unavailable. The key can instead be stored unprotected as a file in the pool folder (0600).",
    storeAsFile: "Store as file",
  },
};


    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
    keychainUnavailableTitle: "No secure storage",
    keychainUnavailable:
      "Keychain/keyring is unavailable. The key can instead be stored unprotected as a file in the pool folder (0600).",
    storeAsFile: "Store as file",
  },
};

import { createI18n } from "vue-i18n";

const de = {
  app: {
    projects: "Projekte",
    pools: "Pools",
    usage: "Verbrauch",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 Tage",
    days30: "30 Tage",
    pool: "Pool",
    project: "Projekt",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ Kosten",
    total: "Summe",
    empty: "Keine Verbrauchsdaten im Zeitraum.",
    estimateNote:
      "API-Gegenwert aus den lokalen Transcripts — keine Abrechnung, Historie nur solange claude sie vorhält.",
  },
  projects: {
    project: "Projekt",
    pool: "Pool",
    noPool: "– kein Pool –",
    start: "Starten",
    stop: "Beenden",
    terminalSettings: "Terminal-Einstellungen",
    stillRunning: "{name} läuft noch",
    poolChangeSaved:
      "Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
    restartHint:
      "Der Neustart beendet die laufende Session. Der automatische commit+push von claude-sync nach Sessionende entfällt dabei.",
    keepRunning: "Weiterlaufen lassen",
    restartNow: "Jetzt neu starten",
    restarting: "Starte neu …",
    terminal: "Terminal — {name}",
    title: "Titel",
    theme: "Theme",
    dockIcon: "Dock-Icon",
    chooseFile: "Datei wählen …",
    remove: "Entfernen",
    defaultIcon: "Standard-Icon",
    appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
    cancel: "Abbrechen",
    save: "Speichern",
    noPoolAssigned: "kein Pool",
    needsPool: "Kein Pool zugeordnet",
    needsKey: "Pool hat keinen API-Key",
    newProject: "+ Projekt",
    wizardTitle: "Neues Projekt",
    name: "Name",
    workDir: "Arbeitsordner",
    workDirNone: "keiner",
    workDirCustom: "vorhandenen Ordner verknüpfen",
    chooseFolder: "Ordner wählen …",
    create: "Anlegen",
    wizardHint:
      "Angelegt werden Projektordner mit memory/, Sentinel-.gitignore, settings.json (Memory-Verzeichnis + Berechtigungen) und die Pool-/Terminal-Zuordnung.",
    delete: "Projekt löschen",
    deleteRunning: "Läuft noch — erst beenden",
    deleteTitle: "{name} löschen",
    deleteWarning:
      "Der Projektordner unter ~/claude-projects (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
    deleteWorkDirs: "Arbeitsordner mitlöschen:",
    workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
    todo: "Todoliste",
    todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
  },
  pools: {
    pool: "Pool",
    type: "Typ",
    actions: "Aktionen",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Zurücksetzen",
    rename: "Umbenennen",
    renameTitle: "{name} umbenennen",
    changeKey: "Key ändern",
    insertKey: "Key eintragen",
    delete: "Löschen",
    assigned: "kein Projekt | 1 Projekt | {count} Projekte",
    empty: "Noch keine Pools angelegt.",
    newOauthPool: "Neuen oAuth-Pool anlegen",
    newApikeyPool: "Neuen API-Key-Pool anlegen",
    editKey: "API-Key ändern – {name}",
    name: "Name",
    apiKey: "API-Key",
    oauthHint:
      "Der Pool wird angelegt; die Anmeldung macht claude beim ersten Start selbst per /login (Browser gegen dein Abo).",
    cancel: "Abbrechen",
    createPool: "Anlegen",
    reset: "Zurücksetzen",
    save: "Speichern",
    deletePool: "Pool löschen",
    deleteWarning:
      "Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
    deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
    deleteBlockedTooltip:
      "Löschen gesperrt — {projects} läuft gerade. Erst beenden.",
    reloginTitle: "{name} zurücksetzen",
    reloginWarning:
      "Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Beim nächsten Start verlangt claude erneut /login.",
    reloginNoEntry:
      "Kein Schlüsselbund-Eintrag vorhanden — beim nächsten Start meldet sich claude ohnehin per /login an.",
    reloginBlocked:
      "Zurücksetzen ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
    keychainUnavailableTitle: "Keine sichere Ablage",
    keychainUnavailable:
      "Keychain/Keyring ist nicht verfügbar. Der Key kann stattdessen ungesichert als Datei im Pool-Ordner liegen (0600).",
    storeAsFile: "Als Datei ablegen",
  },
};

const en: typeof de = {
  app: {
    projects: "Projects",
    pools: "Pools",
    usage: "Usage",
    autostart: "Autostart",
  },
  usage: {
    days7: "7 days",
    days30: "30 days",
    pool: "Pool",
    project: "Project",
    input: "Input",
    output: "Output",
    cacheWrite: "Cache ↑",
    cacheRead: "Cache ↓",
    cost: "≈ cost",
    total: "Total",
    empty: "No usage data in this period.",
    estimateNote:
      "API equivalent from local transcripts — not a bill; history only as long as claude keeps it.",
  },
  projects: {
    project: "Project",
    pool: "Pool",
    noPool: "– no pool –",
    start: "Start",
    stop: "Stop",
    terminalSettings: "Terminal settings",
    stillRunning: "{name} is still running",
    poolChangeSaved:
      "The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
    restartHint:
      "Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped.",
    keepRunning: "Keep running",
    restartNow: "Restart now",
    restarting: "Restarting …",
    terminal: "Terminal — {name}",
    title: "Title",
    theme: "Theme",
    dockIcon: "Dock icon",
    chooseFile: "Choose file …",
    remove: "Remove",
    defaultIcon: "Default icon",
    appliesNextStart: "Applies from the next terminal start.",
    cancel: "Cancel",
    save: "Save",
    noPoolAssigned: "no pool",
    needsPool: "No pool assigned",
    needsKey: "Pool has no API key",
    newProject: "+ Project",
    wizardTitle: "New project",
    name: "Name",
    workDir: "Working folder",
    workDirNone: "none",
    workDirCustom: "link existing folder",
    chooseFolder: "Choose folder …",
    create: "Create",
    wizardHint:
      "Creates the project folder with memory/, sentinel .gitignore, settings.json (memory dir + permissions), and the pool/terminal assignment.",
    delete: "Delete project",
    deleteRunning: "Still running — stop it first",
    deleteTitle: "Delete {name}",
    deleteWarning:
      "The project folder under ~/claude-projects (incl. memory/, settings and pool assignment) will be deleted permanently.",
    deleteWorkDirs: "Also delete working folders:",
    workDirsStay: "The working folders stay untouched.",
    todo: "Todo list",
    todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
  },
  pools: {
    pool: "Pool",
    type: "Type",
    actions: "Actions",
    newOauth: "+ oAuth",
    newApikey: "+ apiKey",
    relogin: "Reset",
    rename: "Rename",
    renameTitle: "Rename {name}",
    changeKey: "Change key",
    insertKey: "Insert key",
    delete: "Delete",
    assigned: "no projects | 1 project | {count} projects",
    empty: "No pools yet.",
    newOauthPool: "Create oAuth pool",
    newApikeyPool: "Create API key pool",
    editKey: "Change API key – {name}",
    name: "Name",
    apiKey: "API key",
    oauthHint:
      "The pool is created; claude signs in itself on first start via /login (browser against your subscription).",
    cancel: "Cancel",
    createPool: "Create",
    reset: "Reset",
    save: "Save",
    deletePool: "Delete pool",
    deleteWarning:
      "Pool {name} and its credential file will be deleted. This cannot be undone.",
    deleteUnassigns: "These projects lose their pool assignment:",
    deleteBlockedTooltip:
      "Deletion blocked — active: {projects}. Stop the session first.",
    reloginTitle: "Reset {name}",
    reloginWarning:
      "The access token for {name} stored in the keychain will be deleted. On the next start claude asks for /login again.",
    reloginNoEntry:
      "No keychain entry present — claude signs in via /login on the next start anyway.",
    reloginBlocked:
      "Resetting requires an unused pool. These projects are still running:",
  },
};

const stored = localStorage.getItem("lang");
const initial =
  stored === "de" || stored === "en"
    ? stored
    : navigator.language.startsWith("de")
      ? "de"
      : "en";

export const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: initial,
  fallbackLocale: "en",
  messages: { de, en },
});

export function setLocale(lang: "de" | "en") {
  i18n.global.locale.value = lang;
  localStorage.setItem("lang", lang);
}

+    keychainUnavailableTitle: "No secure storage",

+      "Keychain/keyring is unavailable. The key can instead be stored unprotected as a file in the pool folder (0600).",


Bash

cd /home/marcuh/projects/ai-control && zsh -ic 'npm run build' 2>&1 | grep -E "error|✓ built" | head

Frontend-Typecheck und Build


✓ built in 197ms


Umgesetzt:

- **Backend:** `create_apikey_pool` und `set_apikey` haben ein `allowFile`-Flag. Schlägt der Store-Write fehl und das Flag ist aus, kommt der Sentinel-Fehler `keychain-unavailable` zurück — bei der Anlage bevor irgendetwas entsteht, beim Key-Ändern bleiben Datei und `settings.json` unangetastet. Mit Flag greift der Datei-Fallback (0600) wie bisher.
- **UI:** erster Versuch immer ohne Datei-Erlaubnis. Auf den Sentinel öffnet sich der Dialog „Keine sichere Ablage" mit dem Hinweis, dass der Key sonst ungesichert als Datei im Pool-Ordner liegt — „Abbrechen" lässt alles stehen (Eingabedialog bleibt offen), „Als Datei ablegen" wiederholt mit `allowFile=true`.

Verifiziert: 48 Rust-Tests grün (neu: Anlage-Abbruch legt nichts an, Änderungs-Abbruch ändert nichts), `vue-tsc` + Build fehlerfrei. Nebenbei behoben: der Form-Handler übergab bisher das Submit-Event als erstes Argument — jetzt expliziter `submit()`-Aufruf, sonst hätte das Event als `allowFile=true` gewirkt.


gut -- alles cmitten / pushen / build und deploy


git status --short && git log --oneline -3 && ls ~/Applications/ | grep -i "ai-control\|\.app" | head; ls /home/marcuh/projects/ai-control/*.sh

Repo-Status, letzte Commits und Deploy-Ziel prüfen


 M build.sh
 M src-tauri/Cargo.lock
 M src-tauri/Cargo.toml
 M src-tauri/icons/128x128.png
 M src-tauri/icons/128x128@2x.png
 M src-tauri/icons/32x32.png
 M src-tauri/icons/64x64.png
 M src-tauri/icons/Square107x107Logo.png
 M src-tauri/icons/Square142x142Logo.png
 M src-tauri/icons/Square150x150Logo.png
 M src-tauri/icons/Square284x284Logo.png
 M src-tauri/icons/Square30x30Logo.png
 M src-tauri/icons/Square310x310Logo.png
 M src-tauri/icons/Square44x44Logo.png
 M src-tauri/icons/Square71x71Logo.png
 M src-tauri/icons/Square89x89Logo.png
 M src-tauri/icons/StoreLogo.png
 M src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
 M src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
 M src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
 M src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
 M src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
 M src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
 M src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
 M src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
 M src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
 M src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
 M src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
 M src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
 M src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
 M src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
 M src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
 M src-tauri/icons/icon.icns
 M src-tauri/icons/icon.ico
 M src-tauri/icons/icon.png
 M src-tauri/icons/ios/AppIcon-20x20@1x.png
 M src-tauri/icons/ios/AppIcon-20x20@2x-1.png
 M src-tauri/icons/ios/AppIcon-20x20@2x.png
 M src-tauri/icons/ios/AppIcon-20x20@3x.png
 M src-tauri/icons/ios/AppIcon-29x29@1x.png
 M src-tauri/icons/ios/AppIcon-29x29@2x-1.png
 M src-tauri/icons/ios/AppIcon-29x29@2x.png
 M src-tauri/icons/ios/AppIcon-29x29@3x.png
 M src-tauri/icons/ios/AppIcon-40x40@1x.png
 M src-tauri/icons/ios/AppIcon-40x40@2x-1.png
 M src-tauri/icons/ios/AppIcon-40x40@2x.png
 M src-tauri/icons/ios/AppIcon-40x40@3x.png
 M src-tauri/icons/ios/AppIcon-512@2x.png
 M src-tauri/icons/ios/AppIcon-60x60@2x.png
 M src-tauri/icons/ios/AppIcon-60x60@3x.png
 M src-tauri/icons/ios/AppIcon-76x76@1x.png
 M src-tauri/icons/ios/AppIcon-76x76@2x.png
 M src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
 M src-tauri/src/lib.rs
 M src-tauri/src/terminal.rs
 M src/components/PoolList.vue
 M src/components/ProjectList.vue
 M src/i18n.ts
 M src/style.css
 M src/terminal.ts
?? src-tauri/icons/trayTemplate.png
09488ae Pool-Runtime (projects/todos/history) als Symlinks ins synced Repo, auch bei Pool-Anlage
3e02231 Ghostty raus, Session-Watcher mit Sync-Opt-in, Tooltip WKWebView-tauglich
20fc847 Pools initialisieren, Pool-Guard raus, Löschschutz bei laufenden Sessions
Claude Code URL Handler.app
HeronVPN.app
WuerthVPN.app
ai-control.app
/home/marcuh/projects/ai-control/build.sh
/home/marcuh/projects/ai-control/dev.sh


git diff --stat src-tauri/src/terminal.rs src/style.css src/terminal.ts build.sh && git diff build.sh src-tauri/src/terminal.rs | head -60

Diffs der nicht von mir stammenden Änderungen ansehen


 build.sh                  |  16 +++-
 src-tauri/src/terminal.rs |  31 ++++++-
 src/style.css             |  13 +++
 src/terminal.ts           | 228 +++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 282 insertions(+), 6 deletions(-)
diff --git a/build.sh b/build.sh
index 457e7fb..cc5b202 100755
--- a/build.sh
+++ b/build.sh
@@ -8,4 +8,18 @@ export RUSTUP_HOME="$HOME/tools/.rustup"
 export PATH="$CARGO_HOME/bin:$PATH"

 cd "$DIR"
-npm run tauri build -- "$@"
+npm run tauri build -- --bundles app "$@"
+
+# Dev-Bundle in ein .noindex-Verzeichnis schieben und aus Launch Services
+# austragen — Spotlight indiziert .noindex-Ordner nicht, damit erscheint im
+# Starter/Spotlight nur die installierte Kopie unter ~/Applications.
+BUNDLE="$DIR/src-tauri/target/release/bundle/macos"
+NOINDEX="$DIR/src-tauri/target/release/bundle/macos.noindex"
+LSREGISTER=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
+# lsregister -u meldet -10814, wenn das Bundle nicht registriert ist —
+# erwartetes Ergebnis, kein Fehler.
+"$LSREGISTER" -u "$BUNDLE/ai-control.app" || true
+rm -rf "$NOINDEX/ai-control.app"
+mkdir -p "$NOINDEX"
+mv "$BUNDLE/ai-control.app" "$NOINDEX/"
+"$LSREGISTER" -u "$NOINDEX/ai-control.app" || true
diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs
index d9c1a15..b4dd5c3 100644
--- a/src-tauri/src/terminal.rs
+++ b/src-tauri/src/terminal.rs
@@ -123,17 +123,46 @@ pub fn term_start(

   let label = window.label().to_string();
   let app = window.app_handle().clone();
+
+  // Output gebündelt emittieren statt pro read(): der Reader schiebt Chunks
+  // in einen Channel, der Emitter sammelt alles, was innerhalb von 8 ms
+  // ankommt, zu einem Event zusammen. Bei Output-Fluten (Scrollen, TUI-
+  // Redraws) sinkt die Zahl der IPC-Events um Größenordnungen; die 8 ms
+  // Zusatzlatenz beim Tasten-Echo liegen unter einem Frame.
+  let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
   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]));
+          if tx.send(buf[..n].to_vec()).is_err() {
+            break;
+          }
         }
       }
     }
     let _ = child.wait();
+    // tx wird hier gedroppt — der Emitter sieht den geschlossenen Channel
+    // erst, nachdem er alle Rest-Chunks emittiert hat.
+  });
+  std::thread::spawn(move || {
+    const FLUSH: std::time::Duration = std::time::Duration::from_millis(8);
+    while let Ok(first) = rx.recv() {
+      let mut chunk = first;


git diff src/terminal.ts | grep "^+" | grep -iE "scrollback|restore|function |const [A-Z_]+ =|resize|search" | head -20; git diff src/style.css | grep "^+" | head -15

terminal.ts- und style.css-Diff grob sichten


+++ b/src/style.css
+.name-row {
+  display: flex;
+  align-items: center;
+  gap: 0.4rem;
+}
+
+.proj-icon {
+  width: 18px;
+  height: 18px;
+  border-radius: 4px;
+  flex: none;
+}
+


git diff src/terminal.ts | grep -E "^\+" | grep -vE "^\+\s*(//|$)" | head -30

terminal.ts-Neuerungen sichten


+++ b/src/terminal.ts
+import "@fontsource/jetbrains-mono/600.css";
+  "solarized-light": {
+    header: "#eee8d5",
+    border: "#93a1a1",
+    xterm: {
+      background: "#fdf6e3",
+      foreground: "#657b83",
+      cursor: "#657b83",
+      cursorAccent: "#fdf6e3",
+      selectionBackground: "#eee8d5b0",
+      black: "#073642",
+      red: "#dc322f",
+      green: "#859900",
+      yellow: "#b58900",
+      blue: "#268bd2",
+      magenta: "#d33682",
+      cyan: "#2aa198",
+      white: "#eee8d5",
+      brightBlack: "#002b36",
+      brightRed: "#cb4b16",
+      brightGreen: "#586e75",
+      brightYellow: "#657b83",
+      brightBlue: "#839496",
+      brightMagenta: "#6c71c4",
+      brightCyan: "#93a1a1",
+      brightWhite: "#fdf6e3",
+    },
+  },
+  "catppuccin-latte": {


Zwei Commits (Bestand von gestern + heutige Pool-Arbeit), dann push, build, deploy:


git add build.sh src-tauri/icons src-tauri/src/terminal.rs src/style.css src/terminal.ts && git commit -m "Terminal: Output-Batching (8 ms) + helle Themes, Projekt-Icons in der Liste, neues App-Icon inkl. Tray-Template, build.sh: Dev-Bundle nach .noindex" && git add -A && git commit -m "Pools: Anzeigename von Pool-ID (UUID-Ordner) getrennt + Umbenennen; API-Keys in Keychain/Keyring, Datei-Fallback nur nach Rückfrage" && git log --oneline -2

Änderungen in zwei Commits aufteilen


[main 23aa315] Terminal: Output-Batching (8 ms) + helle Themes, Projekt-Icons in der Liste, neues App-Icon inkl. Tray-Template, build.sh: Dev-Bundle nach .noindex
 55 files changed, 282 insertions(+), 6 deletions(-)
 create mode 100644 src-tauri/icons/trayTemplate.png
[main be6c56d] Pools: Anzeigename von Pool-ID (UUID-Ordner) getrennt + Umbenennen; API-Keys in Keychain/Keyring, Datei-Fallback nur nach Rückfrage
 6 files changed, 851 insertions(+), 144 deletions(-)
be6c56d Pools: Anzeigename von Pool-ID (UUID-Ordner) getrennt + Umbenennen; API-Keys in Keychain/Keyring, Datei-Fallback nur nach Rückfrage
23aa315 Terminal: Output-Batching (8 ms) + helle Themes, Projekt-Icons in der Liste, neues App-Icon inkl. Tray-Template, build.sh: Dev-Bundle nach .noindex


Commits pushen


remote: Processed 1 references in total
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   09488ae..be6c56d  main -> main


Release-Build der App (build.sh)


       Built application at: /home/marcuh/projects/ai-control/src-tauri/target/release/ai-control
    Bundling ai-control.app (/home/marcuh/projects/ai-control/src-tauri/target/release/bundle/macos/ai-control.app)
    Finished 1 bundle at:
        /home/marcuh/projects/ai-control/src-tauri/target/release/bundle/macos/ai-control.app

failed to scan /home/marcuh/projects/ai-control/src-tauri/target/release/bundle/macos/ai-control.app: -10814
 from spotlightfailed to scan /home/marcuh/projects/ai-control/src-tauri/target/release/bundle/macos.noindex/ai-control.app: -10814
 from spotlight


rm -rf ~/Applications/ai-control.app && ditto /home/marcuh/projects/ai-control/src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ai-control.app && ls -d ~/Applications/ai-control.app && pgrep -fl "ai-control.app" | head -3

Neue App-Version nach ~/Applications kopieren


/home/marcuh/Applications/ai-control.app
58322 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control --terminal misc


Erledigt:

- **Commits:** `23aa315` (Terminal-Batching, helle Themes, Projekt-Icons, App-Icon, build.sh-noindex — Bestand von gestern) und `be6c56d` (Pool-ID/Anzeigename + Keychain-Ablage) — gepusht.
- **Build:** `./build.sh` fehlerfrei, Bundle liegt unter `macos.noindex`.
- **Deploy:** `~/Applications/ai-control.app` ersetzt.

Es läuft noch eine Instanz der alten Version (`--terminal misc`, aktive Terminal-Session) — die habe ich bewusst nicht angefasst; die neue Version greift beim nächsten App-Start.


ok -- die app ist kapuut
die terminals starten nicht mehr


- 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


Ich schaue mir Logs und den Terminal-Startpfad an — ich starte selbst kein Terminal.


ls -lat ~/Library/Logs/ai-control/ 2>/dev/null | head; grep -n "terminal" /home/marcuh/projects/ai-control/src-tauri/src/main.rs

Log-Verzeichnis und Terminal-Startpfad finden


(Bash completed with no output)


cat /home/marcuh/projects/ai-control/src-tauri/src/main.rs; grep -n "open_terminal\|--terminal" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | head

main.rs und open_terminal-Stellen ansehen


// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
  app_lib::run();
}
106:/// PIDs der eingebauten Terminal-Prozesse (`app --terminal <projekt>`).
109:    .args(["-f", "--", &format!("--terminal {project}$")])
1405:  terminal::open_terminal(project)
1435:    // `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
1437:    Some("--terminal") => {
1438:      let project = args.next().expect("--terminal braucht einen Projektnamen");
1557:      terminal::open_terminal


1425	  }
1426	  link_pool_runtime_in(&paths, &pool)
1427	}
1428
1429	#[cfg_attr(mobile, tauri::mobile_entry_point)]
1430	pub fn run() {
1431	  // generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST).
1432	  let context = tauri::generate_context!();
1433	  let mut args = std::env::args().skip(1);
1434	  match args.next().as_deref() {
1435	    // `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
1436	    // damit jedes Terminal ein eigenes Dock-Icon bekommt.
1437	    Some("--terminal") => {
1438	      let project = args.next().expect("--terminal braucht einen Projektnamen");
1439	      let icon = terminal_config(&project)
1440	        .expect("Projekt-Config nicht lesbar")
1441	        .icon
1442	        .map(|i| {
1443	          resolve_icon_path(&Paths::real(), &project, &i)
1444	            .to_string_lossy()
1445	            .into_owned()
1446	        });
1447	      terminal_builder(project)
1448	        .build(context)
1449	        .expect("error while building tauri application")
1450	        // Das Dock-Icon erst nach dem App-Start setzen: in setup() gesetzt
1451	        // überschreibt macOS es beim Anlegen des Dock-Tiles wieder.
1452	        .run(move |_app, event| {
1453	          if let tauri::RunEvent::Ready = event {
1454	            if let Some(icon) = icon.as_deref() {
1455	              terminal::set_dock_icon(icon);
1456	            }
1457	          }
1458	        });
1459	    }
1460	    _ => main_builder()
1461	      .run(context)
1462	      .expect("error while running tauri application"),
1463	  }
1464	}
1465
1466	/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
1467	fn main_builder() -> tauri::Builder<tauri::Wry> {
1468	  use tauri::Manager;
1469
1470	  tauri::Builder::default()
1471	    .plugin(tauri_plugin_autostart::init(
1472	      tauri_plugin_autostart::MacosLauncher::LaunchAgent,
1473	      None,
1474	    ))
1475	    .plugin(tauri_plugin_dialog::init())
1476	    .setup(|app| {
1477	      if cfg!(debug_assertions) {
1478	        app.handle().plugin(
1479	          tauri_plugin_log::Builder::default()

  }
  link_pool_runtime_in(&paths, &pool)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  // generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST).
  let context = tauri::generate_context!();
  let mut args = std::env::args().skip(1);
  match args.next().as_deref() {
    // `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
    // damit jedes Terminal ein eigenes Dock-Icon bekommt.
    Some("--terminal") => {
      let project = args.next().expect("--terminal braucht einen Projektnamen");
      let icon = terminal_config(&project)
        .expect("Projekt-Config nicht lesbar")
        .icon
        .map(|i| {
          resolve_icon_path(&Paths::real(), &project, &i)
            .to_string_lossy()
            .into_owned()
        });
      terminal_builder(project)
        .build(context)
        .expect("error while building tauri application")
        // Das Dock-Icon erst nach dem App-Start setzen: in setup() gesetzt
        // überschreibt macOS es beim Anlegen des Dock-Tiles wieder.
        .run(move |_app, event| {
          if let tauri::RunEvent::Ready = event {
            if let Some(icon) = icon.as_deref() {
              terminal::set_dock_icon(icon);
            }
          }
        });
    }
    _ => main_builder()
      .run(context)
      .expect("error while running tauri application"),
  }
}

/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
fn main_builder() -> tauri::Builder<tauri::Wry> {
  use tauri::Manager;

  tauri::Builder::default()
    .plugin(tauri_plugin_autostart::init(
      tauri_plugin_autostart::MacosLauncher::LaunchAgent,
      None,
    ))
    .plugin(tauri_plugin_dialog::init())
    .setup(|app| {
      if cfg!(debug_assertions) {
        app.handle().plugin(
          tauri_plugin_log::Builder::default()


grep -n "open_terminal" /home/marcuh/projects/ai-control/src-tauri/src/terminal.rs | head; sed -n '1,60p' /home/marcuh/projects/ai-control/src-tauri/src/terminal.rs

Terminal-Spawn-Code ansehen


23:pub fn open_terminal(project: String) -> Result<(), String> {
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 {


ok -- die app ist kapuut  die terminals starten nicht mehr


ls -lat ~/Library/Logs/DiagnosticReports/ 2>/dev/null | grep -i ai-control | head -5

Crash-Reports der App suchen


log show --last 20m --predicate 'process == "ai-control" OR (eventMessage CONTAINS "ai-control")' 2>/dev/null | grep -viE "wifi|bluetooth" | tail -40

Systemlog der letzten 20 Minuten nach ai-control filtern


pgrep -fl ai-control | head; log show --last 45m --predicate 'processImagePath CONTAINS "ai-control"' --style compact 2>/dev/null | tail -30

Laufende Prozesse und Log mit processImagePath-Filter


git -C /home/marcuh/projects/ai-control show 23aa315 -- src-tauri/src/terminal.rs | head -100

Terminal.rs-Änderungen aus Commit 23aa315 ansehen


commit 23aa315b8f5b381f22b3b0135b0266e41a664d77
Author: marcus.hinz <marcus.hinz@adesso.de>
Date:   Sun Jul 5 12:16:16 2026 +0200

    Terminal: Output-Batching (8 ms) + helle Themes, Projekt-Icons in der Liste, neues App-Icon inkl. Tray-Template, build.sh: Dev-Bundle nach .noindex

diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs
index d9c1a15..b4dd5c3 100644
--- a/src-tauri/src/terminal.rs
+++ b/src-tauri/src/terminal.rs
@@ -123,17 +123,46 @@ pub fn term_start(

   let label = window.label().to_string();
   let app = window.app_handle().clone();
+
+  // Output gebündelt emittieren statt pro read(): der Reader schiebt Chunks
+  // in einen Channel, der Emitter sammelt alles, was innerhalb von 8 ms
+  // ankommt, zu einem Event zusammen. Bei Output-Fluten (Scrollen, TUI-
+  // Redraws) sinkt die Zahl der IPC-Events um Größenordnungen; die 8 ms
+  // Zusatzlatenz beim Tasten-Echo liegen unter einem Frame.
+  let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
   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]));
+          if tx.send(buf[..n].to_vec()).is_err() {
+            break;
+          }
         }
       }
     }
     let _ = child.wait();
+    // tx wird hier gedroppt — der Emitter sieht den geschlossenen Channel
+    // erst, nachdem er alle Rest-Chunks emittiert hat.
+  });
+  std::thread::spawn(move || {
+    const FLUSH: std::time::Duration = std::time::Duration::from_millis(8);
+    while let Ok(first) = rx.recv() {
+      let mut chunk = first;
+      let deadline = std::time::Instant::now() + FLUSH;
+      loop {
+        let now = std::time::Instant::now();
+        if now >= deadline {
+          break;
+        }
+        match rx.recv_timeout(deadline - now) {
+          Ok(more) => chunk.extend_from_slice(&more),
+          Err(_) => break,
+        }
+      }
+      let _ = app.emit_to(&label, "pty-output", STANDARD.encode(&chunk));
+    }
     let _ = app.emit_to(&label, "pty-exit", ());
   });


git -C /home/marcuh/projects/ai-control show 23aa315 -- src/terminal.ts | grep -E "^[+-]" | grep -vE "^[+-]{3}" | grep -vE "^[+-]\s*(//|$)" | grep -viE '":\s*"#|header:|border:|background:|foreground:|cursor|selection|black|red|green|yellow|blue|magenta|cyan|white' | head -60

terminal.ts-Diff ohne Farbdefinitionen sichten


+import "@fontsource/jetbrains-mono/600.css";
+  "solarized-light": {
+    xterm: {
+    },
+  },
+  "catppuccin-latte": {
+    xterm: {
+    },
+  },
+  "one-light": {
+    xterm: {
+    },
+  },
+  nord: {
+    xterm: {
+    },
+  },
+  "tokyo-night": {
+    xterm: {
+    },
+  },
+  monokai: {
+    xterm: {
+    },
+  },
+  "rose-pine": {
+    xterm: {
+    },
+  },
+  everforest: {
+    xterm: {
+    },
+  },
-  lineHeight: 1.25,
+  lineHeight: 1.0,
+await document.fonts.load('600 13px "JetBrains Mono"');


~/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 2>&1 | head -5; echo "exit: $?"

Binary-Start ohne Projektname testen (erwarteter kontrollierter Panic, kein Fenster)


thread 'main' (359159) panicked at src/lib.rs:1438:33:
--terminal braucht einen Projektnamen
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit: 0


git -C /home/marcuh/projects/ai-control show be6c56d -- src-tauri/src/lib.rs | grep -E "^[+-]" | grep -vE "^[+-]{3}" | grep -iE "icon|terminal|window|builder|dock" | head -30

Icon-/Terminal-relevante lib.rs-Änderungen im Commit sichten


-  terminal: TerminalConfig,
+  mut terminal: TerminalConfig,
+  // Absolut gewählte Icons in den Projektordner kopieren und relativ
+  // speichern — so wird das Icon mit dem Projekt-Repo gesynct.
+  if let Some(icon) = terminal.icon.as_deref() {
+    if icon.starts_with('/') {
+      let src = PathBuf::from(icon);
+      let name = format!("icon.{ext}");
+      terminal.icon = Some(name);
+/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im Projektordner.
+fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> PathBuf {
+  if icon.starts_with('/') {
+    PathBuf::from(icon)
+    paths.projects_dir().join(project).join(icon)
+/// Icon eines Projekts als PNG-data-URL für die Übersicht; ICNS wird per
+fn project_icon(project: String) -> Result<Option<String>, String> {
+  let Some(icon) = read_project_config_in(&paths, &project)?.terminal.icon else {
+  let path = resolve_icon_path(&paths, &project, &icon);
+    let tmp = std::env::temp_dir().join(format!("ai-control-icon-{project}.png"));
-/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den
+/// (Terminal-Einstellungen bleiben erhalten). Den Schutz gegen laufende
-        .icon;
+        .icon
+          resolve_icon_path(&Paths::real(), &project, &i)
-          "../icons/128x128.png"
+          "../icons/trayTemplate.png"
+      project_icon,


~/Applications/ai-control.app/Contents/MacOS/ai-control 2>&1 | head -10 &
sleep 5; pgrep -fl "ai-control$" | head -3

Haupt-App testweise starten (Tray-App, kein Terminal) und Überleben prüfen


63076 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control


kill 63076; cat ~/.local/bin/claude-sync

Test-Instanz beenden und claude-sync ansehen


#!/usr/bin/env bash
# claude-sync — git pull before Claude, commit+push after.
# Safe on conflicts: never merges/forces; just reports and lets you resolve.
# The MiniTerm app cd's into the project dir first, so git runs on the CWD.
set -uo pipefail

# Aus dem GUI gestartet (ghostty-App) erbt der Prozess nur den launchd-PATH
# (/usr/bin:/bin:…) — ~/.local/bin fehlt, also würde `claude` unten nicht
# gefunden und das Skript sofort enden. Eigenes Verzeichnis (dort liegt der
# claude-Symlink) voranstellen.
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PATH="$self_dir:$PATH"

ok()   { printf '\033[1;32m✓ %s\033[0m\n'  "$*"; }   # green
note() { printf '\033[1;36m· %s\033[0m\n'  "$*"; }   # cyan
warn() { printf '\033[1;33m⚠ %s\033[0m\n'  "$*"; }   # yellow

# --- Pool auflösen (integriert aus claude-start) -----------------------------
# Liest die Pool-Zuordnung aus ./ai-control.json (CWD = Projektordner, den der
# Starter setzt) und richtet CLAUDE_CONFIG_DIR auf den Pool-Ordner.
pool=$(sed -n 's/.*"pool"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' ai-control.json 2>/dev/null)
[ -n "$pool" ] || { warn "kein Pool in $PWD/ai-control.json"; sleep 5; exit 1; }
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-control/pools/$pool"
note "Pool: $pool"

# --- Sentinel: markiert die laufende Pool-Session im Projekt-Root ------------
# Liegt beim Start noch eine Sentinel, sind das Reste einer hart beendeten
# Session (Schreiber, Zombies) — killen und wegräumen. Doppelstarts verhindert
# die App über ihre Lauf-Erkennung (--terminal-PIDs).
sentinel="$PWD/.ai-control-running"
if [ -e "$sentinel" ]; then
  wpid=$(sed -n 's/^pid=//p' "$sentinel")
  holders=$(lsof -t -- "$sentinel" 2>/dev/null)
  kill -9 $wpid $holders 2>/dev/null
  rm -f "$sentinel"
fi
printf 'pool=%s\npid=%s\n' "$CLAUDE_CONFIG_DIR" "$$" >"$sentinel"
exec 9<"$sentinel"

# Fenster-X / App-Quit (HUP/TERM): claude beenden und Sentinel löschen — es
# überlebt nichts unsichtbar. INT (Ctrl+C) gehört claude, das Skript ignoriert es.
trap 'rm -f "$sentinel"' EXIT
trap ':' INT
trap 'kill "${claude_pid:-}" 2>/dev/null; exit 129' HUP TERM

# --- Is this a git repo with a remote? -------------------------------------
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  warn "Kein Git-Repo hier — starte Claude ohne Sync."
  claude "$@" <&0 &
  claude_pid=$!
  wait "$claude_pid"
  rc=$?
  sleep 3
  exit "$rc"
fi
have_remote=0
git remote get-url origin >/dev/null 2>&1 && have_remote=1
branch=$(git rev-parse --abbrev-ref HEAD)

# --- PULL before start ------------------------------------------------------
if [[ "$have_remote" -eq 1 ]]; then
  git fetch --quiet origin "$branch" 2>/dev/null || true
  if git rev-parse --verify --quiet "origin/$branch" >/dev/null; then
    behind=$(git rev-list --count "HEAD..origin/$branch" 2>/dev/null || echo 0)
    if [[ "$behind" -eq 0 ]]; then
      ok "git pull: bereits aktuell ($branch)."
    elif git merge --ff-only "origin/$branch" >/dev/null 2>&1; then
      ok "git pull: $behind Commit(s) geholt ($branch)."
    else
      warn "git pull übersprungen — kein sauberer Fast-Forward (lokale Änderungen oder divergierte History). Bitte manuell lösen."
    fi
  else
    note "Kein Remote-Branch '$branch' — Pull übersprungen."
  fi
else
  note "Kein origin-Remote — Sync übersprungen."
fi

# --- run Claude -------------------------------------------------------------
# Im Hintergrund + wait, damit HUP/TERM den Trap sofort erreichen (bash würde
# sonst hinter einem claude, das HUP überlebt, festhängen). <&0 erhält das
# Terminal als stdin — Hintergrund-Kinder bekämen sonst /dev/null.
claude "$@" <&0 &
claude_pid=$!
wait "$claude_pid"
claude_rc=$?
rm -f "$sentinel"

# --- PUSH after end ---------------------------------------------------------
if [[ "$have_remote" -eq 1 ]]; then
  # Commit any working-tree changes (CLAUDE.md, memories, ...) so they can be pushed.
  if [[ -n "$(git status --porcelain)" ]]; then
    git add -A
    git commit --quiet -m "claude sync: $(date '+%Y-%m-%d %H:%M')" && \
      note "Änderungen committed."
  fi
  if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
    ahead=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
    if [[ "$ahead" -eq 0 ]]; then
      note "git push: nichts zu pushen."
    elif git push --quiet 2>/dev/null; then
      ok "git push: $ahead Commit(s) gepusht."
    else
      warn "git push abgelehnt — Remote hat neue Commits. Bitte manuell mergen/pushen."
    fi
  else
    if git push --quiet -u origin "$branch" 2>/dev/null; then
      ok "git push: Upstream gesetzt und gepusht."
    else
      warn "Kein Upstream gesetzt, automatischer Push fehlgeschlagen — bitte einmal manuell pushen."
    fi
  fi
fi

# Schlussmeldungen kurz anzeigen, dann schließt das Fenster (wait-after-command=false).
sleep 3
exit "$claude_rc"


grep -n "fn terminal_builder\|fn terminal_config" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs; sed -n "$(grep -n 'fn terminal_builder' /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | cut -d: -f1),+45p" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs

terminal_builder ansehen


578:pub(crate) fn terminal_config(project: &str) -> Result<TerminalConfig, String> {
1563:fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
  tauri::Builder::default()
    .manage(terminal::Terminals::default())
    .setup(move |app| {
      let cfg = terminal_config(&project)?;
      terminal::build_window(app.handle(), &project, &cfg)?;
      Ok(())
    })
    // Fenster zu → PTY-Session abräumen; danach endet der Prozess.
    .on_window_event(|window, event| {
      if let tauri::WindowEvent::Destroyed = event {
        terminal::close(window);
      }
    })
    .invoke_handler(tauri::generate_handler![
      terminal::term_start,
      terminal::term_log,
      terminal::term_write,
      terminal::term_resize,
      // Pool-Chip im Terminal-Header braucht die Projektliste auch hier.
      list_projects
    ])
}

// ---------- Tests ----------

#[cfg(test)]
mod tests {
  use super::*;
  use std::sync::atomic::{AtomicU32, Ordering};

  static N: AtomicU32 = AtomicU32::new(0);

  /// Frisches Pseudo-Home pro Test; claude-projects existiert wie auf dem
  /// echten System.
  fn tmp_paths() -> Paths {
    let dir = std::env::temp_dir().join(format!(
      "ai-control-test-{}-{}",
      std::process::id(),
      N.fetch_add(1, Ordering::SeqCst)
    ));
    fs::create_dir_all(dir.join("claude-projects")).unwrap();
    Paths { home: dir }
  }

  /// In-Memory-ApikeyStore statt echtem Keychain.


grep -n "pub fn term_log\|pub fn term_start\|pub fn build_window" /home/marcuh/projects/ai-control/src-tauri/src/terminal.rs; grep -n "invoke(\|term_start\|term_log\|list_projects" /home/marcuh/projects/ai-control/src/terminal.ts | head -20

Terminal-Handshake Rust/TS abgleichen


68:pub fn build_window(
98:pub fn term_start(
178:pub fn term_log(window: tauri::WebviewWindow, msg: String) {
20:  invoke("term_log", { msg });
397:const projects = await invoke<Project[]>("list_projects");
435:    if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
461:term.onData((data) => invoke("term_write", { data }));
462:term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
465:await invoke("term_start", { project, rows: term.rows, cols: term.cols });


sed -n "$(grep -n 'fn list_projects_in' /home/marcuh/projects/ai-control/src-tauri/src/lib.rs | cut -d: -f1),+40p" /home/marcuh/projects/ai-control/src-tauri/src/lib.rs

list_projects_in ansehen


fn list_projects_in(paths: &Paths) -> Result<Vec<Project>, String> {
  let mut projects = Vec::new();
  let entries = fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())?;
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    let path = entry.path();
    if !path.join(".claude").is_dir() {
      continue;
    }
    let name = entry.file_name().to_string_lossy().into_owned();
    let cfg = read_project_config_in(paths, &name)?;
    projects.push(Project {
      running: is_running(&name),
      path: path.to_string_lossy().into_owned(),
      pool: cfg.pool,
      terminal: cfg.terminal,
      name,
    });
  }
  projects.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(projects)
}

// ---------- Feature: Todoliste ----------

/// Muster robotunits: Datei im Projekt-Root, per SessionStart-Hook (jq)
/// als additionalContext in jede Session injiziert.
const TODO_FILE: &str = "OFFENE-PUNKTE.md";
const TODO_SKELETON: &str = "# Offene Punkte — bei jedem Start prüfen und abhaken\n\nKeine offenen Punkte.\n";

fn todo_hook_command(paths: &Paths, name: &str) -> String {
  format!(
    "jq -Rs '{{systemMessage: ., hookSpecificOutput:{{hookEventName:\"SessionStart\", additionalContext: .}}}}' {}",
    paths.projects_dir().join(name).join(TODO_FILE).display()
  )
}

fn settings_path(paths: &Paths, name: &str) -> PathBuf {
  paths
    .projects_dir()
    .join(name)


330	        {{ $t("projects.pool") }}
331
332

333

334

335

336
337
338
339
340
341
342	            {{ p.name }}
343
344	          {{ p.path }}
345
346
347	          <select :value="p.pool ?? ''" @change="assign(p, $event)">
348	            <option value="" disabled>{{ $t("projects.noPool") }}</option>
349	            <option v-for="pool in pools" :key="pool.id" :value="pool.id">
350	              {{ pool.name }}
351	            </option>
352	          </select>
353
354
355
356	            <button
357	              class="gear"
358	              :title="$t('projects.terminalSettings')"
359	              @click="openSettings(p)"
360	            >
361	              ⚙
362	            </button>
363	            <button
364	              class="gear danger"
365	              :disabled="p.running"
366	              :title="p.running ? $t('projects.deleteRunning') : $t('projects.delete')"
367	              @click="askDelete(p)"
368	            >
369	              🗑
370	            </button>
371	            <button v-if="p.running" class="stop" @click="stop(p)">
372	              {{ $t("projects.stop") }}
373	            </button>
374	            <button
375	              v-else
376	              class="start"
377	              :disabled="!poolReady(p)"
378	              :title="startBlockedReason(p) && $t(startBlockedReason(p)!)"
379	              @click="openTerminal(p)"
380	            >
381	              {{ $t("projects.start") }}
382	            </button>
383
384
385

386

387

388
389

390	    <form class="dialog" @submit.prevent="createProject">
391
{{ $t("projects.wizardTitle") }}

392
393	        {{ $t("projects.name") }}
394	        <input v-model="wizard.name" autofocus required />
395
396
397	        {{ $t("projects.pool") }}
398	        <select v-model="wizard.pool">
399	          <option value="">{{ $t("projects.noPool") }}</option>

{{ $t("projects.pool") }}

            {{ p.name }}

          {{ p.path }}

          <select :value="p.pool ?? ''" @change="assign(p, $event)">
            <option value="" disabled>{{ $t("projects.noPool") }}</option>
            <option v-for="pool in pools" :key="pool.id" :value="pool.id">
              {{ pool.name }}
            </option>
          </select>

            <button
              class="gear"
              :title="$t('projects.terminalSettings')"
              @click="openSettings(p)"
            >
              ⚙
            </button>
            <button
              class="gear danger"
              :disabled="p.running"
              :title="p.running ? $t('projects.deleteRunning') : $t('projects.delete')"
              @click="askDelete(p)"
            >
              🗑
            </button>
            <button v-if="p.running" class="stop" @click="stop(p)">
              {{ $t("projects.stop") }}
            </button>
            <button
              v-else
              class="start"
              :disabled="!poolReady(p)"
              :title="startBlockedReason(p) && $t(startBlockedReason(p)!)"
              @click="openTerminal(p)"
            >
              {{ $t("projects.start") }}
            </button>

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

{{ $t("projects.wizardTitle") }}

        {{ $t("projects.name") }}
        <input v-model="wizard.name" autofocus required />

        {{ $t("projects.pool") }}
        <select v-model="wizard.pool">
          <option value="">{{ $t("projects.noPool") }}</option>


for f in ~/claude-projects/*/ai-control.json; do echo "== $f"; cat "$f"; done

Alle Projekt-Configs ansehen


== /home/marcuh/claude-projects/08-07/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "one-dark",
    "icon": "/home/marcuh/projects/claude-app-builder-4-mac/icons/08-07.png",
    "title": "08-07"
  }
}
== /home/marcuh/claude-projects/ai-control/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "monokai",
    "icon": "/home/marcuh/Downloads/artificial-intelligence-ai-icon.png"
  }
}
== /home/marcuh/claude-projects/limbach/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "dracula",
    "icon": "/home/marcuh/projects/claude-app-builder-4-mac/icons/limbach.png"
  }
}
== /home/marcuh/claude-projects/misc/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "tokyo-night",
    "icon": "/home/marcuh/claude-projects/misc/icon-m.png"
  }
}
== /home/marcuh/claude-projects/robotunits/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "nord",
    "icon": "icon.png"
  }
}
== /home/marcuh/claude-projects/wittgenstein/ai-control.json
{
  "pool": "private",
  "terminal": {
    "theme": "catppuccin-latte",
    "icon": "icon.png"
  }
}
== /home/marcuh/claude-projects/wuerth-plato/ai-control.json
{
  "pool": "private",
  "terminal": {
    "icon": "icon.png"
  }
}


Existenz aller Icon-Dateien prüfen


-rw-r--r--@ 1 marcus.hinz  staff   54791 Jul  5 10:50 /home/marcuh/claude-projects/misc/icon-m.png
-rwx------  1 marcus.hinz  staff   29711 Jun 10 13:20 /home/marcuh/claude-projects/robotunits/icon.png
-rwx------  1 marcus.hinz  staff   98754 May 31 22:20 /home/marcuh/claude-projects/wittgenstein/icon.png
-rwx------  1 marcus.hinz  staff   35623 May 31 23:34 /home/marcuh/claude-projects/wuerth-plato/icon.png
-rw-r--r--@ 1 marcus.hinz  staff   17955 Jul  5 09:51 /home/marcuh/Downloads/artificial-intelligence-ai-icon.png
-rw-r--r--  1 marcus.hinz  staff  152365 Jun 30 13:43 /home/marcuh/projects/claude-app-builder-4-mac/icons/08-07.png
-rwx------  1 marcus.hinz  staff   28309 Jun  7 17:30 /home/marcuh/projects/claude-app-builder-4-mac/icons/limbach.png


ls /home/marcuh/projects/ai-control/src-tauri/capabilities/ && cat /home/marcuh/projects/ai-control/src-tauri/capabilities/*.json && git -C /home/marcuh/projects/ai-control log --oneline -3 -- src-tauri/capabilities/

Tauri-Capabilities prüfen


default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "enables the default permissions",
  "windows": [
    "main",
    "term-*"
  ],
  "permissions": [
    "core:default",
    "core:window:allow-start-dragging",
    "autostart:default",
    "dialog:default"
  ]
}
fc560d4 Terminal-Config (Theme/Icon/Titel), UI-Überarbeitung, i18n de/en
f747379 Eingebautes Terminal (xterm.js + portable-pty) und Start/Beenden-Button
70e4898 beta-0.8


[Request interrupted by user]


Du kannst Terminals starten
du läufst komplet außerhalb der App

stand?


cd ~/claude-projects/08-07 && ~/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 08-07 > /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6/scratchpad/term-test.log 2>&1 &
echo "gestartet pid $!"; sleep 6; pgrep -fl -- "--terminal 08-07"; echo "--- log:"; cat /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6/scratchpad/term-test.log

Terminal-Prozess für 08-07 starten und Ausgabe beobachten


Du kannst Terminals starten du läufst komplet außerhalb der App  stand?


gestartet pid 64502
64502 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 08-07
--- log:
Shell cwd was reset to /home/marcuh/claude-projects/ai-control


ps -o pid,ppid,command -ax | grep -E "64502|claude-sync|zsh -ic" | grep -v grep; ls -la ~/claude-projects/08-07/.ai-control-running 2>/dev/null && cat ~/claude-projects/08-07/.ai-control-running

Kindprozesse des Terminal-Prozesses und Sentinel prüfen


64502     1 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 08-07
64564 64502 bash /home/marcuh/.local/bin/claude-sync
-rw-r--r--  1 marcus.hinz  staff  67 Jul  5 12:34 /home/marcuh/claude-projects/08-07/.ai-control-running
pool=/home/marcuh/.config/ai-control/pools/private
pid=64564


kill 64502; sleep 2; env -i HOME="$HOME" USER="$USER" LOGNAME="$USER" SHELL=/bin/zsh PATH="/usr/bin:/bin:/usr/sbin:/sbin" TMPDIR="$TMPDIR" /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 08-07 > /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6/scratchpad/term-test2.log 2>&1 &
TESTPID=$!; echo "pid $TESTPID"; sleep 8; pgrep -fl -- "--terminal 08-07"; echo "--- log:"; cat /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/eba397b0-eb36-44d0-85c7-70fcc86c17c6/scratchpad/term-test2.log; ps -o pid,ppid,command -ax | grep claude-sync | grep -v grep

Terminal mit launchd-ähnlicher Minimal-Umgebung starten


pid 64813
64813 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control --terminal 08-07
--- log:
64826 64813 bash /home/marcuh/.local/bin/claude-sync


kill 64813 2>/dev/null; sleep 1; pgrep -fl -- "--terminal" ; open -a /home/marcuh/Applications/ai-control.app; sleep 4; pgrep -fl "ai-control" | head

Test-Terminal beenden, App über Launch Services starten


64984 /home/marcuh/Applications/ai-control.app/Contents/MacOS/ai-control


Stand der Diagnose — von außen funktioniert alles:

- Das deployte Binary startet sauber (Arg-Parsing, kein Signatur-Problem).
- `--terminal 08-07` direkt gestartet: Fenster-Prozess lebt, claude-sync lief, Sentinel/Pool korrekt — sowohl mit meiner Shell-Umgebung als auch mit launchd-Minimal-Umgebung. Testprozesse habe ich wieder beendet.
- Alle Projekt-Configs sind valide, alle Icon-Dateien vorhanden, Capabilities unverändert.
- Die Haupt-App läuft jetzt frisch über Launch Services (von mir gestartet, pid 64984).

Den Klick auf „Starten" in der UI kann ich nicht selbst auslösen — das ist das einzige noch ungetestete Glied. Klick jetzt einmal auf Starten; ich beobachte parallel Prozesse und stderr und sehe dann, wo es hakt. Falls dabei eine Fehlermeldung in der App erscheint: Wortlaut reicht mir.


jetzt sieht es gut aus
moment


Gut. Ich warte.


das betrifft tatsächlich nur ai-control
die andedren funktioniern
Außerhalb der App kann ich ai-control öffnen (offensichtlich) inApp führt zu einem bstautz


- 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


Das erklärt es — ich prüfe die Crash-Reports von eben:


ls -lat ~/Library/Logs/DiagnosticReports/ 2>/dev/null | head -8

Frische Crash-Reports suchen


(Bash completed with no output)


ls -lat ~/Library/Logs/DiagnosticReports/ | head -8

DiagnosticReports auflisten


ls: /home/marcuh/Library/Logs/DiagnosticReports/: No such file or directory


ls -lat /Library/Logs/DiagnosticReports/ 2>/dev/null | grep -i ai | head -5; ls ~/Library/Logs/ 2>/dev/null | head

Alternative Log-Orte prüfen


Assistant
Baseband
Claude
com.aicontrol.app
com.apple.AMPLibraryAgent
com.apple.CloudTelemetry
Company Portal
CoreSimulator
CrashReporter
Cyberduck


ls -lat ~/Library/Logs/com.aicontrol.app/ | head -5; tail -30 ~/Library/Logs/com.aicontrol.app/*.log 2>/dev/null | tail -35

App-Logverzeichnis ansehen


total 0
drwxr-xr-x   3 marcus.hinz  staff    96 Jul  3 10:19 .
-rw-r--r--   1 marcus.hinz  staff     0 Jul  3 10:19 ai-control.log
drwx------+ 34 marcus.hinz  staff  1088 Jul  3 10:19 ..


ls -lat ~/Library/Logs/CrashReporter/ 2>/dev/null | head; ls -lat "$HOME/Library/Logs/DiagnosticReports" 2>/dev/null | head -6; find /Library/Logs/DiagnosticReports -iname "*ai-control*" -newer /tmp -maxdepth 1 2>/dev/null | head

CrashReporter-Verzeichnisse durchsuchen


total 0
drwx------+ 34 marcus.hinz  staff  1088 Jul  3 10:19 ..
drwxr-xr-x   2 marcus.hinz  staff    64 Jun 25 10:40 MobileDevice
drwxr-xr-x   4 marcus.hinz  staff   128 Jun 18 08:54 .
drwxr-xr-x   3 marcus.hinz  staff    96 May 12 07:18 DiagnosticLogs


sqlite3 ~/Library/"Application Support"/com.apple.TCC/TCC.db "select service, client, auth_value, last_modified from access where client like '%aicontrol%' or client like '%ai-control%';" 2>&1

TCC-Berechtigungen der App abfragen


Error: Exit code 1
Error: unable to open database "/home/marcuh/Library/Application Support/com.apple.TCC/TCC.db": authorization denied


das betrifft tatsächlich nur ai-control die andedren funktioniern Außerhalb der App kann ich ai-control öffnen (offensichtlich) inApp führt zu einem bstautz


Gefunden. Es ist das Icon:

- `set_dock_icon` paniced hart, wenn `NSImage` die Icon-Datei nicht laden kann — und das läuft in `RunEvent::Ready`, direkt beim Start jedes Terminal-Prozesses.
- ai-control ist das einzige Projekt, dessen Icon in `~/Downloads` liegt (`artificial-intelligence-ai-icon.png`) — ein TCC-geschützter Ordner. Alle anderen Projekte haben ihre Icons unter `~/projects/…` oder im Projektordner, deshalb funktionieren sie.
- In-App gespawnt ist ai-control.app selbst der responsible process für TCC; ohne Downloads-Berechtigung liefert NSImage nil → Panic → Absturz. Das Deploy hat zudem die Signatur erneuert, ein früher erteilter Grant wäre damit hinfällig.
- Von außerhalb gestartet erbt der Prozess die TCC-Rechte des startenden Terminals — Icon lesbar, kein Crash. Deckt sich exakt mit deinem Befund.

Zwei Ebenen: als Sofortfix das Icon in der ai-control-Config auf eine Kopie im Projektordner umstellen (`icon-aic.png` liegt dort schon); grundsätzlich die Frage, ob ein nicht ladbares Icon den Terminal-Start weiter mit Panic beenden soll oder das Terminal ohne Dock-Icon starten lassen darf.


deshalb wollte die App aud Downlodsa zugreifen
1 - ich dachte, du hättestes die icons in die Configs kopiert
2 - gegen so was muss man rubust sein


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,

#[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,


Edit

  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}"));

  let mtm = MainThreadMarker::new().expect("set_dock_icon läuft nicht auf dem Main-Thread");
  // Nicht ladbar (Datei weg, TCC-geschützter Ordner wie ~/Downloads): Terminal
  // ohne eigenes Dock-Icon starten statt den Prozess zu beenden.
  let Some(img) = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path))
  else {
    eprintln!("Dock-Icon nicht ladbar, Terminal startet ohne: {path}");
    return;
  };