/// Verlegt den Projektordner: Registry-Eintrag auf den neuen Pfad; Verweise
/// auf den alten Root in der settings.json (autoMemoryDirectory,
/// Edit-Permission) ziehen mit. Verschoben wird nichts — der neue Ordner
/// muss existieren.
fn set_project_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
  check_name(name)?;
  let new_dir = expand_home(paths, dir);
  if !new_dir.is_dir() {
    return Err(format!("Ordner nicht gefunden: {}", new_dir.display()));
  }
  let mut reg = load_registry(paths)?;
  let old_dir = reg
    .get(name)
    .cloned()
    .ok_or_else(|| format!("Projekt nicht registriert: {name}"))?;
  if old_dir == new_dir {
    return Ok(());
  }
  reg.insert(name.to_string(), new_dir.clone());
  save_registry(paths, &reg)?;

  let sp = settings_path(&new_dir);
  if !sp.is_file() {
    return Ok(()); // importierte Projekte ohne settings.json
  }
  let old_c = contract_home(paths, &old_dir);
  let new_c = contract_home(paths, &new_dir);
  let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
  let mut v: serde_json::Value =
    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?;
  if let Some(mem) = v["autoMemoryDirectory"].as_str() {
    if let Some(rest) = mem.strip_prefix(&old_c) {
      v["autoMemoryDirectory"] = serde_json::json!(format!("{new_c}{rest}"));
    }
  }
  if let Some(allow) = v["permissions"]["allow"].as_array_mut() {
    let old_edit = format!("Edit({old_c}/**)");
    for e in allow.iter_mut() {
      if e.as_str() == Some(&old_edit) {
        *e = serde_json::json!(format!("Edit({new_c}/**)"));
      }
    }
  }
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}

/// Nimmt einen bestehenden Ordner als Projekt auf; der Name ist der Ordnername.

+/// Verlegt den Projektordner: Registry-Eintrag auf den neuen Pfad; Verweise

+/// auf den alten Root in der settings.json (autoMemoryDirectory,

+/// Edit-Permission) ziehen mit. Verschoben wird nichts — der neue Ordner

+fn set_project_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {

+  let new_dir = expand_home(paths, dir);

+    return Err(format!("Ordner nicht gefunden: {}", new_dir.display()));

+    .ok_or_else(|| format!("Projekt nicht registriert: {name}"))?;

+  reg.insert(name.to_string(), new_dir.clone());

+    return Ok(()); // importierte Projekte ohne settings.json

+  let old_c = contract_home(paths, &old_dir);

+  let new_c = contract_home(paths, &new_dir);

+  let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;

+    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?;

+  if let Some(mem) = v["autoMemoryDirectory"].as_str() {

+    if let Some(rest) = mem.strip_prefix(&old_c) {

+      v["autoMemoryDirectory"] = serde_json::json!(format!("{new_c}{rest}"));

+  if let Some(allow) = v["permissions"]["allow"].as_array_mut() {

+    let old_edit = format!("Edit({old_c}/**)");

+      if e.as_str() == Some(&old_edit) {

+        *e = serde_json::json!(format!("Edit({new_c}/**)"));

+  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;

+  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))

fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> {


Edit

#[tauri::command]
fn add_work_dir(project: String, dir: String) -> Result<(), String> {

/// Projektordner neu zuordnen; bei laufender Session gesperrt.
#[tauri::command]
fn set_project_dir(project: String, dir: String) -> Result<(), String> {
  if is_running(&project) {
    return Err(format!("{project} läuft noch — erst beenden"));
  }
  set_project_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn add_work_dir(project: String, dir: String) -> Result<(), String> {


The file /Users/marcus.hinz/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)

/// Projektordner neu zuordnen; bei laufender Session gesperrt.
#[tauri::command]
fn set_project_dir(project: String, dir: String) -> Result<(), String> {
  if is_running(&project) {
    return Err(format!("{project} läuft noch — erst beenden"));
  }
  set_project_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn add_work_dir(project: String, dir: String) -> Result<(), String> {

+/// Projektordner neu zuordnen; bei laufender Session gesperrt.

+fn set_project_dir(project: String, dir: String) -> Result<(), String> {

+    return Err(format!("{project} läuft noch — erst beenden"));

+  set_project_dir_in(&Paths::real(), &project, &dir)


      project_work_dirs,
      add_work_dir,
      remove_work_dir,

      project_work_dirs,
      set_project_dir,
      add_work_dir,
      remove_work_dir,


      project_work_dirs,
      add_work_dir,
      remove_work_dir,

      project_work_dirs,
      set_project_dir,
      add_work_dir,
      remove_work_dir,


damit kann man den Working Folder eines Projekts nicht mehr ändern das ist der Root, in dem das Projekt gesatrtet wird warum nicht?


TryMenü für Projektliste mit Start/Stop-Button


  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {

  #[test]
  fn projektordner_verlegen() {
    let p = tmp_paths();
    create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)
      .unwrap();
    // Nutzer hat den Ordner selbst verschoben; die App ordnet nur neu zu.
    let new_dir = p.home.join("elsewhere").join("proj");
    fs::create_dir_all(p.home.join("elsewhere")).unwrap();
    fs::rename(p.projects_dir().join("proj"), &new_dir).unwrap();
    set_project_dir_in(&p, "proj", &new_dir.to_string_lossy()).unwrap();

    assert_eq!(project_dir(&p, "proj").unwrap(), new_dir);
    let raw = fs::read_to_string(settings_path(&new_dir)).unwrap();
    assert!(raw.contains("~/elsewhere/proj/memory"));
    assert!(raw.contains("Edit(~/elsewhere/proj/**)"));
    assert!(!raw.contains("claude-projects/proj"));
  }

  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {


  #[test]
  fn projektordner_verlegen() {
    let p = tmp_paths();
    create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)
      .unwrap();
    // Nutzer hat den Ordner selbst verschoben; die App ordnet nur neu zu.
    let new_dir = p.home.join("elsewhere").join("proj");
    fs::create_dir_all(p.home.join("elsewhere")).unwrap();
    fs::rename(p.projects_dir().join("proj"), &new_dir).unwrap();
    set_project_dir_in(&p, "proj", &new_dir.to_string_lossy()).unwrap();

    assert_eq!(project_dir(&p, "proj").unwrap(), new_dir);
    let raw = fs::read_to_string(settings_path(&new_dir)).unwrap();
    assert!(raw.contains("~/elsewhere/proj/memory"));
    assert!(raw.contains("Edit(~/elsewhere/proj/**)"));
    assert!(!raw.contains("claude-projects/proj"));
  }

  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {

+    create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)

+    // Nutzer hat den Ordner selbst verschoben; die App ordnet nur neu zu.

+    let new_dir = p.home.join("elsewhere").join("proj");

+    fs::create_dir_all(p.home.join("elsewhere")).unwrap();

+    fs::rename(p.projects_dir().join("proj"), &new_dir).unwrap();

+    set_project_dir_in(&p, "proj", &new_dir.to_string_lossy()).unwrap();

+    assert_eq!(project_dir(&p, "proj").unwrap(), new_dir);

+    let raw = fs::read_to_string(settings_path(&new_dir)).unwrap();

+    assert!(raw.contains("~/elsewhere/proj/memory"));

+    assert!(raw.contains("Edit(~/elsewhere/proj/**)"));

+    assert!(!raw.contains("claude-projects/proj"));


// Arbeitsordner schreiben direkt in die Projekt-settings.json

// Projektordner neu zuordnen (Root, in dem die Session startet); verschoben
// wird nichts — der gewählte Ordner ist der neue Ort des Projekts.
async function changeProjectDir() {
  const s = settings.value!;
  const dir = await open({ directory: true, multiple: false });
  if (typeof dir !== "string") return;
  try {
    await invoke("set_project_dir", { project: s.name, dir });
    await refresh();
    const p = projects.value.find((x) => x.name === s.name);
    if (p) s.path = p.path;
  } catch (e) {
    error.value = String(e);
  }
}

// Arbeitsordner schreiben direkt in die Projekt-settings.json


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

// Projektordner neu zuordnen (Root, in dem die Session startet); verschoben
// wird nichts — der gewählte Ordner ist der neue Ort des Projekts.
async function changeProjectDir() {
  const s = settings.value!;
  const dir = await open({ directory: true, multiple: false });
  if (typeof dir !== "string") return;
  try {
    await invoke("set_project_dir", { project: s.name, dir });
    await refresh();
    const p = projects.value.find((x) => x.name === s.name);
    if (p) s.path = p.path;
  } catch (e) {
    error.value = String(e);
  }
}

// Arbeitsordner schreiben direkt in die Projekt-settings.json

+// Projektordner neu zuordnen (Root, in dem die Session startet); verschoben

+// wird nichts — der gewählte Ordner ist der neue Ort des Projekts.

+  const dir = await open({ directory: true, multiple: false });

+    await invoke("set_project_dir", { project: s.name, dir });

+    const p = projects.value.find((x) => x.name === s.name);

// (additionalDirectories + Edit-Permission) — kein Speichern-Schritt.


            {{ settings.path }}
            <button @click="changeProjectDir">{{ $t("projects.changeDir") }}</button>


            {{ settings.path }}
            <button @click="changeProjectDir">{{ $t("projects.changeDir") }}</button>

+            <button @click="changeProjectDir">{{ $t("projects.changeDir") }}</button>

<span v-for="wd in settings.workDirs" :key="wd" class="icon-row">


    addWorkDir: "Ordner hinzufügen …",
    changeDir: "Ändern …",


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

    addWorkDir: "Ordner hinzufügen …",
    changeDir: "Ändern …",

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",
    addFolder: "Projekt importieren …",
    addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
    addWorkDir: "Ordner hinzufügen …",
    wizardTitle: "Neues Projekt",
    name: "Name",
    projectDir: "Projektordner",
    projectDirCustom: "Ordner wählen …",
    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 (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
    deleteWorkDirs: "Arbeitsordner mitlöschen:",
    workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
    unlink: "Nur aus Liste nehmen",
    unlinkHint:
      "Nur aus Liste nehmen entfernt den Eintrag; der Ordner bleibt 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",
    addFolder: "Import project …",
    addFolderTitle: "Add an existing folder as a project",
    addWorkDir: "Add folder …",
    wizardTitle: "New project",
    name: "Name",
    projectDir: "Project folder",
    projectDirCustom: "Choose folder …",
    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 (incl. memory/, settings and pool assignment) will be deleted permanently.",
    deleteWorkDirs: "Also delete working folders:",
    workDirsStay: "The working folders stay untouched.",
    unlink: "Remove from list only",
    unlinkHint:
      "Remove from list only drops the entry; the folder stays 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:",
    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",
  },
};

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


    addWorkDir: "Add folder …",
    changeDir: "Change …",


    addWorkDir: "Add folder …",
    changeDir: "Change …",

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",
    addFolder: "Projekt importieren …",
    addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
    addWorkDir: "Ordner hinzufügen …",
    changeDir: "Ändern …",
    wizardTitle: "Neues Projekt",
    name: "Name",
    projectDir: "Projektordner",
    projectDirCustom: "Ordner wählen …",
    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 (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
    deleteWorkDirs: "Arbeitsordner mitlöschen:",
    workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
    unlink: "Nur aus Liste nehmen",
    unlinkHint:
      "Nur aus Liste nehmen entfernt den Eintrag; der Ordner bleibt 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",
    addFolder: "Import project …",
    addFolderTitle: "Add an existing folder as a project",
    addWorkDir: "Add folder …",
    wizardTitle: "New project",
    name: "Name",
    projectDir: "Project folder",
    projectDirCustom: "Choose folder …",
    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 (incl. memory/, settings and pool assignment) will be deleted permanently.",
    deleteWorkDirs: "Also delete working folders:",
    workDirsStay: "The working folders stay untouched.",
    unlink: "Remove from list only",
    unlinkHint:
      "Remove from list only drops the entry; the folder stays 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:",
    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",
  },
};

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


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup" PATH="$HOME/tools/.cargo/bin:$PATH"; cargo test --manifest-path src-tauri/Cargo.toml 2>&1 | grep -E "test result|FAILED|panicked" -A 2 | head -8 && npm run build 2>&1 | grep -E "error|✓ built" | head -3 && ./build.sh 2>&1 | tail -1 && rm -rf ~/Applications/ai-control.app && cp -R src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ && /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/ai-control.app && echo DEPLOYED

Test, build and deploy project-dir change feature


test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.22s

     Running unittests src/main.rs (src-tauri/target/debug/deps/ai_control-cb387f5a798b9c01)
--
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests app_lib
--
✓ built in 222ms
 from spotlightDEPLOYED


Deployed, 50/50 Tests grün. Der Projektordner im ⚙-Dialog hat jetzt einen „Ändern …"-Button: Picker → Registry-Eintrag zeigt auf den gewählten Ordner, `autoMemoryDirectory` und die `Edit(<root>/**)`-Permission in der settings.json werden auf den neuen Root umgeschrieben. Verschieben musst du den Ordner vorher selbst; bei laufender Session ist das Ändern gesperrt. Nach App-Neustart testbar; Commit auf Go.


go


git add -A && git commit -m 'Projektordner im ⚙-Dialog änderbar: Registry-Eintrag neu zuordnen, autoMemoryDirectory + Edit-Permission ziehen auf den neuen Root um; gesperrt bei laufender Session' && git push 2>&1 | tail -2

Commit and push project-dir change feature


[main 0ebdaf0] Projektordner im ⚙-Dialog änderbar: Registry-Eintrag neu zuordnen, autoMemoryDirectory + Edit-Permission ziehen auf den neuen Root um; gesperrt bei laufender Session
 3 files changed, 94 insertions(+)
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   2075f9a..0ebdaf0  main -> main


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

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

Seit 2026-07-03 produktiv:

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

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

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

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

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

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


No-op


(Bash completed with no output)


Edit

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

**Stand 2026-07-05 (Session GitHub-Release, alles committed bis 0ebdaf0):**
- **Projekt-Registry statt festem Root:** `~/.config/ai-control/projects.json` (Name → Pfad, Home-Pfade ~-kontrahiert), Alt-Layout-Scan komplett entfernt — ohne Registry keine Projekte. Marcus' 7 Projekte migriert. Neue Commands add_project (Import bestehender Ordner, UI „Projekt importieren …"), remove_project (nur Eintrag), set_project_dir (Root im ⚙-Dialog änderbar; schreibt autoMemoryDirectory + Edit-Permission um). Arbeitsordner nachträglich im ⚙-Dialog erfassbar/entfernbar (additionalDirectories + Edit-Permission, ~-kontrahiert).
- **Icons zentral:** `~/.config/ai-control/icons/<projekt>.<ext>` (NICHT Pool — Marcus' Entscheidung: gemeinsames Resourcen-Verzeichnis, kein Umzug bei Pool-Wechsel, nichts im Quell-Repo). Alle 7 Projekte migriert. Nicht ladbares Icon crasht den Tray nicht mehr (eprintln + Eintrag ohne Icon; vorher Crash-Ursache: gelöschtes icon.png).
- **Terminal:** `claudeCommand` in App-settings.json (Default `claude`; bei Marcus `claude-sync`), App setzt CLAUDE_CONFIG_DIR aus dem Projekt-Pool selbst. Terminal-Fenster aktiviert sich beim Öffnen (Starter yieldActivationToApplicationWithBundleIdentifier + activate im Ready-Event — Aktivierung seit macOS 14 kooperativ, sonst Pop-under). Tray-Menü listet Projekte mit Icon + grünem Statuspunkt (56×36-RGBA im Icon-Slot, muda skaliert auf 18 pt); Klick startet bzw. fokussiert (NSRunningApplication über PID).
- **Session-Sync:** Watcher synct per Projekt das umgebende Git-Repo (add/commit/pull --rebase/push inline), git-sync-Skript-Kopplung raus.
- **GitHub-reif:** README englisch mit Screenshots (docs/), MIT-Lizenz, Anthropic-Lizenz-Hinweis (reine Pool-Verwaltung, OAuth-Login macht claude selbst). Deploy-Falle: nach rm -rf+cp nach ~/Applications `lsregister -f` nötig, sonst „App weg" in Spotlight.
- 50 Rust-Tests. Zwischenfall: Commit im falschen Repo (Shell-cwd springt auf ~/claude-projects/ai-control zurück — bei git-Befehlen immer explizit cd ~/projects/ai-control), per amend+force-with-lease bereinigt.

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


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

**Stand 2026-07-05 (Session GitHub-Release, alles committed bis 0ebdaf0):**
- **Projekt-Registry statt festem Root:** `~/.config/ai-control/projects.json` (Name → Pfad, Home-Pfade ~-kontrahiert), Alt-Layout-Scan komplett entfernt — ohne Registry keine Projekte. Marcus' 7 Projekte migriert. Neue Commands add_project (Import bestehender Ordner, UI „Projekt importieren …"), remove_project (nur Eintrag), set_project_dir (Root im ⚙-Dialog änderbar; schreibt autoMemoryDirectory + Edit-Permission um). Arbeitsordner nachträglich im ⚙-Dialog erfassbar/entfernbar (additionalDirectories + Edit-Permission, ~-kontrahiert).
- **Icons zentral:** `~/.config/ai-control/icons/<projekt>.<ext>` (NICHT Pool — Marcus' Entscheidung: gemeinsames Resourcen-Verzeichnis, kein Umzug bei Pool-Wechsel, nichts im Quell-Repo). Alle 7 Projekte migriert. Nicht ladbares Icon crasht den Tray nicht mehr (eprintln + Eintrag ohne Icon; vorher Crash-Ursache: gelöschtes icon.png).
- **Terminal:** `claudeCommand` in App-settings.json (Default `claude`; bei Marcus `claude-sync`), App setzt CLAUDE_CONFIG_DIR aus dem Projekt-Pool selbst. Terminal-Fenster aktiviert sich beim Öffnen (Starter yieldActivationToApplicationWithBundleIdentifier + activate im Ready-Event — Aktivierung seit macOS 14 kooperativ, sonst Pop-under). Tray-Menü listet Projekte mit Icon + grünem Statuspunkt (56×36-RGBA im Icon-Slot, muda skaliert auf 18 pt); Klick startet bzw. fokussiert (NSRunningApplication über PID).
- **Session-Sync:** Watcher synct per Projekt das umgebende Git-Repo (add/commit/pull --rebase/push inline), git-sync-Skript-Kopplung raus.
- **GitHub-reif:** README englisch mit Screenshots (docs/), MIT-Lizenz, Anthropic-Lizenz-Hinweis (reine Pool-Verwaltung, OAuth-Login macht claude selbst). Deploy-Falle: nach rm -rf+cp nach ~/Applications `lsregister -f` nötig, sonst „App weg" in Spotlight.
- 50 Rust-Tests. Zwischenfall: Commit im falschen Repo (Shell-cwd springt auf ~/claude-projects/ai-control zurück — bei git-Befehlen immer explizit cd ~/projects/ai-control), per amend+force-with-lease bereinigt.

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

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

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

+**Stand 2026-07-05 (Session GitHub-Release, alles committed bis 0ebdaf0):**

+- **Projekt-Registry statt festem Root:** `~/.config/ai-control/projects.json` (Name → Pfad, Home-Pfade ~-kontrahiert), Alt-Layout-Scan komplett entfernt — ohne Registry keine Projekte. Marcus' 7 Projekte migriert. Neue Commands add_project (Import bestehender Ordner, UI „Projekt importieren …"), remove_project (nur Eintrag), set_project_dir (Root im ⚙-Dialog änderbar; schreibt autoMemoryDirectory + Edit-Permission um). Arbeitsordner nachträglich im ⚙-Dialog erfassbar/entfernbar (additionalDirectories + Edit-Permission, ~-kontrahiert).

+- **Icons zentral:** `~/.config/ai-control/icons/<projekt>.<ext>` (NICHT Pool — Marcus' Entscheidung: gemeinsames Resourcen-Verzeichnis, kein Umzug bei Pool-Wechsel, nichts im Quell-Repo). Alle 7 Projekte migriert. Nicht ladbares Icon crasht den Tray nicht mehr (eprintln + Eintrag ohne Icon; vorher Crash-Ursache: gelöschtes icon.png).

+- **Terminal:** `claudeCommand` in App-settings.json (Default `claude`; bei Marcus `claude-sync`), App setzt CLAUDE_CONFIG_DIR aus dem Projekt-Pool selbst. Terminal-Fenster aktiviert sich beim Öffnen (Starter yieldActivationToApplicationWithBundleIdentifier + activate im Ready-Event — Aktivierung seit macOS 14 kooperativ, sonst Pop-under). Tray-Menü listet Projekte mit Icon + grünem Statuspunkt (56×36-RGBA im Icon-Slot, muda skaliert auf 18 pt); Klick startet bzw. fokussiert (NSRunningApplication über PID).

+- **Session-Sync:** Watcher synct per Projekt das umgebende Git-Repo (add/commit/pull --rebase/push inline), git-sync-Skript-Kopplung raus.

+- **GitHub-reif:** README englisch mit Screenshots (docs/), MIT-Lizenz, Anthropic-Lizenz-Hinweis (reine Pool-Verwaltung, OAuth-Login macht claude selbst). Deploy-Falle: nach rm -rf+cp nach ~/Applications `lsregister -f` nötig, sonst „App weg" in Spotlight.

+- 50 Rust-Tests. Zwischenfall: Commit im falschen Repo (Shell-cwd springt auf ~/claude-projects/ai-control zurück — bei git-Befehlen immer explizit cd ~/projects/ai-control), per amend+force-with-lease bereinigt.

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

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

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


- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte

- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control (pools, projects.json-Registry, icons, settings.json mit claudeCommand); eingebautes Terminal + Tray mit Statuspunkten; Stand 2026-07-05 GitHub-reif (README en, MIT, Screenshots); Deploy braucht lsregister


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	# Memory Index
2
3	- [Go-Pflicht](feedback-go-pflicht.md) — weitreichende Änderungen nie ohne ausdrückliches Go; eine Rückfrage von Marcus ist kein Go (Vorfall 2026-07-04)
4	- [Terminal-Tests mit 08-07](feedback-terminal-test-08-07.md) — ABSOLUTES VERBOT: nie ein Terminal für/aus ai-control starten (killt die eigene Session, 3× Kontext verloren am 2026-07-04). Terminal-Tests nur mit 08-07 und nur auf ausdrückliche Ansage
5
6	- [Multi-Account & Keychain](multi-account-keychain.md) — Stand 2.1.199: suffixierter Keychain-Eintrag pro Config-Dir verifiziert; Pools umbenannt, Keychain-Endstand ein Eintrag 096c4ef9 (privateDefault); Kaltstart über Starter läuft (2026-07-04), Qualität laut Marcus noch nicht zufriedenstellend
7	- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte
8

# Memory Index

- [Go-Pflicht](feedback-go-pflicht.md) — weitreichende Änderungen nie ohne ausdrückliches Go; eine Rückfrage von Marcus ist kein Go (Vorfall 2026-07-04)
- [Terminal-Tests mit 08-07](feedback-terminal-test-08-07.md) — ABSOLUTES VERBOT: nie ein Terminal für/aus ai-control starten (killt die eigene Session, 3× Kontext verloren am 2026-07-04). Terminal-Tests nur mit 08-07 und nur auf ausdrückliche Ansage

- [Multi-Account & Keychain](multi-account-keychain.md) — Stand 2.1.199: suffixierter Keychain-Eintrag pro Config-Dir verifiziert; Pools umbenannt, Keychain-Endstand ein Eintrag 096c4ef9 (privateDefault); Kaltstart über Starter läuft (2026-07-04), Qualität laut Marcus noch nicht zufriedenstellend
- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte


- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte

- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control (pools, projects.json-Registry, icons, settings.json mit claudeCommand); eingebautes Terminal + Tray mit Statuspunkten; Stand 2026-07-05 GitHub-reif (README en, MIT, Screenshots); Deploy braucht lsregister


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

# Memory Index

- [Go-Pflicht](feedback-go-pflicht.md) — weitreichende Änderungen nie ohne ausdrückliches Go; eine Rückfrage von Marcus ist kein Go (Vorfall 2026-07-04)
- [Terminal-Tests mit 08-07](feedback-terminal-test-08-07.md) — ABSOLUTES VERBOT: nie ein Terminal für/aus ai-control starten (killt die eigene Session, 3× Kontext verloren am 2026-07-04). Terminal-Tests nur mit 08-07 und nur auf ausdrückliche Ansage

- [Multi-Account & Keychain](multi-account-keychain.md) — Stand 2.1.199: suffixierter Keychain-Eintrag pro Config-Dir verifiziert; Pools umbenannt, Keychain-Endstand ein Eintrag 096c4ef9 (privateDefault); Kaltstart über Starter läuft (2026-07-04), Qualität laut Marcus noch nicht zufriedenstellend
- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte

-- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control/pools, Starter, alle Projekte auf privateDefault; seit 2026-07-04 eingebautes Terminal in der App (xterm.js + portable-pty, startet claude-sync), offene Punkte

+- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control (pools, projects.json-Registry, icons, settings.json mit claudeCommand); eingebautes Terminal + Tray mit Statuspunkten; Stand 2026-07-05 GitHub-reif (README en, MIT, Screenshots); Deploy braucht lsregister


Commit `0ebdaf0` gepusht — Repo und App sind synchron. Memory habe ich auf den heutigen Stand gebracht (Registry, zentrale Icons, claudeCommand, GitHub-Reife, lsregister-Falle).

Damit ist alles aus dieser Session committed. Für den GitHub-Release fehlt nur noch das Anlegen des Repos dort und der Push — das Repo liegt ja bisher auf deinem Gitea (`git.lintus.art`).


ok -- vielleicht noch ins Readme, dass die apiKey in die KeyChain kommen
Wir raten dringend von fallback sie ins config Verzeicnis zu legen ab


For that reason the app stores no OAuth tokens itself: with an OAuth pool, the first session of a project on that pool goes through the regular Anthropic login (`/login`, browser, against your subscription), run by Claude Code itself. Credentials live where Claude Code puts them — in the macOS keychain, one entry per pool.

For that reason the app stores no OAuth tokens itself: with an OAuth pool, the first session of a project on that pool goes through the regular Anthropic login (`/login`, browser, against your subscription), run by Claude Code itself. Credentials live where Claude Code puts them — in the macOS keychain, one entry per pool.

API keys go into the keychain as well (one entry per pool). Only when no keychain/keyring is available does the app offer — after an explicit confirmation — to store the key as a file in the pool directory (mode 0600). We strongly advise against that fallback: an API key on disk is readable by every process running as your user. Use the keychain.


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

For that reason the app stores no OAuth tokens itself: with an OAuth pool, the first session of a project on that pool goes through the regular Anthropic login (`/login`, browser, against your subscription), run by Claude Code itself. Credentials live where Claude Code puts them — in the macOS keychain, one entry per pool.

API keys go into the keychain as well (one entry per pool). Only when no keychain/keyring is available does the app offer — after an explicit confirmation — to store the key as a file in the pool directory (mode 0600). We strongly advise against that fallback: an API key on disk is readable by every process running as your user. Use the keychain.

# ai-control

Pool and session management for [Claude Code](https://claude.com/claude-code) on macOS — a tray app with a built-in terminal. Linux and Windows integration to follow.

> All code in this project was generated by Claude (Claude Code).

<img src="docs/projects.png" width="640" alt="Projects">

## Purpose

Running Claude Code with multiple accounts or credential sets means hitting the right `CLAUDE_CONFIG_DIR` (plus keychain entry) for every session. ai-control turns that into clicks:

- **Pools** — named credential sets (OAuth login or API key) under `~/.config/ai-control/pools/<pool>`. Each pool is a self-contained Claude config directory with its own keychain entry.
- **Projects** — arbitrary directories, mapped through a registry (`~/.config/ai-control/projects.json`, name → path; paths under home are stored as `~/…` and therefore stable across machines). Each project is assigned a pool, plus terminal settings (theme, icon, window title).
- **Sessions** — a built-in terminal per project (xterm.js + PTY) that launches Claude Code with the pool's config directory as `CLAUDE_CONFIG_DIR`. Every terminal runs as its own process with its own Dock icon and Cmd-Tab entry.
- **Tray** — the app itself is a pure menu-bar app without a Dock entry. The tray menu lists all projects with icon and status dot (green = running); clicking starts the project or brings the running terminal to the front.
- **Session watcher** — detects the end of a session by the terminal process disappearing and then (opt-in) syncs the Git repository containing the project: add → commit → pull --rebase → push.

## Screenshots

| Pools | Usage |
|---|---|
| ![Pools](docs/pools.png) | ![Usage](docs/usage.png) |

| Tray menu | Session terminal |
|---|---|
| ![Tray menu](docs/tray.png) | ![Session terminal](docs/terminal.png) |

## Project layout

```
├── index.html            main UI (project/pool management)
├── terminal.html         terminal window (xterm.js)
├── src/                  frontend: Vue 3 + TypeScript
├── src-tauri/
│   ├── src/lib.rs        Tauri commands: projects, pools, keychain,
│   │                     OAuth login, tray menu, session watcher
│   ├── src/terminal.rs   PTY sessions (portable-pty), terminal windows,
│   │                     Dock icon, focusing running terminals
│   └── icons/            app and tray icons
├── dev.sh                development mode (tauri dev)
└── build.sh              release build (.app bundle)
```

Two process roles from one binary:

- **Main app** (`ai-control`) — tray, main window, management, watcher.
- **Terminal process** (`ai-control --terminal <project>`) — one window with its own PTY; launches Claude Code in the project directory with the assigned pool's `CLAUDE_CONFIG_DIR`.

Running projects are detected through their terminal processes (`pgrep` for `--terminal <project>`), without a state file.

## Tooling

| Area      | Stack |
|-----------|-------|
| Shell     | Tauri 2 (Rust) |
| Frontend  | Vue 3, TypeScript, Vite |
| Terminal  | xterm.js, portable-pty |
| macOS     | objc2 / objc2-app-kit (Dock icon, window focus, tray) |
| Secrets   | keyring (macOS Keychain) |

## Development

```sh
./dev.sh      # tauri dev with hot reload
./build.sh    # release build; bundle lands in src-tauri/target/release/bundle/macos.noindex/
```

`build.sh` moves the bundle into a `.noindex` directory and unregisters it from Launch Services so Spotlight only finds the installed copy. Install by copying `ai-control.app` to `~/Applications`.

Prerequisites: Rust (stable), Node.js/npm, macOS. `dev.sh`/`build.sh` expect `CARGO_HOME`/`RUSTUP_HOME` under `~/tools/` — adjust the two exports in the scripts for a standard installation.

## Configuration layout

```
~/.config/ai-control/
├── settings.json                 app settings (see below)
├── projects.json                 project registry: name → directory
└── pools/<pool>/                 one Claude config directory per pool
    └── pool.json                 name + credential type

<project directory>/              any path, mapped via the registry
├── .claude/                      Claude Code project configuration
└── ai-control.json               pool assignment + terminal settings
```

### settings.json

| Key                | Default  | Meaning |
|--------------------|----------|---------|
| `claudeCommand`    | `claude` | Command launched in the project terminal (via `zsh -ic`, so your `.zshrc` PATH applies). |
| `syncOnSessionEnd` | `false`  | After a session ends, commit and push the Git repository containing the project. |

## Claude and Anthropic licensing

ai-control is pool management only — it does not change, extend, or circumvent Anthropic's licensing in any way. Using Claude Code through ai-control is subject to the same Anthropic terms as running it directly.

For that reason the app stores no OAuth tokens itself: with an OAuth pool, the first session of a project on that pool goes through the regular Anthropic login (`/login`, browser, against your subscription), run by Claude Code itself. Credentials live where Claude Code puts them — in the macOS keychain, one entry per pool.

## License

[MIT](LICENSE)

+API keys go into the keychain as well (one entry per pool). Only when no keychain/keyring is available does the app offer — after an explicit confirmation — to store the key as a file in the pool directory (mode 0600). We strongly advise against that fallback: an API key on disk is readable by every process running as your user. Use the keychain.


Bash

git add README.md && git commit -m 'README: API-Keys liegen im Keychain; ausdrückliche Warnung vor dem Datei-Fallback im Pool-Verzeichnis' && git push 2>&1 | tail -2

Commit and push README credentials note


[main 8a831af] README: API-Keys liegen im Keychain; ausdrückliche Warnung vor dem Datei-Fallback im Pool-Verzeichnis
 1 file changed, 2 insertions(+)
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   0ebdaf0..8a831af  main -> main


Drin und gepusht (`8a831af`) — im Abschnitt „Claude and Anthropic licensing": API-Keys landen im Keychain (ein Eintrag pro Pool); der Datei-Fallback im Pool-Verzeichnis existiert nur ohne verfügbaren Keychain, nur nach ausdrücklicher Bestätigung, und das README rät ausdrücklich davon ab — mit Begründung (Key auf Platte ist für jeden Prozess deines Users lesbar).


gut - jetzt fällt mit auf Anhieb nichts mehr ein
hast du noch was?


Vier Punkte hätte ich noch, in absteigender Relevanz:

1. **History:** Der Plan aus einer früheren Session war, den Ist-Stand als Initial-Commit **ohne** History auf GitHub zu legen. Die jetzige History ist unkritisch (nur Code, keine Secrets), enthält aber deutsche Commit-Messages und den Weg über deine private Infrastruktur. Entscheidung: History mitnehmen oder squashen.
2. **`link_pool_runtime`:** zeigt hart auf `~/claude-projects/pool/<pool>` — dein persönliches Sync-Konstrukt steckt damit im öffentlichen Code. Funktioniert bei niemand anderem; entweder rausnehmen, konfigurierbar machen oder als bewusst „works for my workflow" stehen lassen.
3. **Versionierung:** Cargo 0.1.0, package.json 0.0.0, `repository = ""` in Cargo.toml. Vor dem Release auf eine Version einigen (z. B. 0.1.0 überall), Repository-URL eintragen, Tag setzen.
4. **Verteilung:** Das README sagt „build it yourself" — dabei kann es bleiben. Nur falls du gebaute `.app`-Bundles als GitHub-Release anhängen willst: ohne Signierung + Notarisierung blockt Gatekeeper sie bei fremden Nutzern.

Punkt 1 und 3 würde ich vor dem Push nach GitHub klären, 2 kannst du auch danach entscheiden, 4 nur bei Binary-Releases.


History --> raus
link_pool_runtime ist?


`link_pool_runtime` ist das Kommando hinter deinem Session-Sync über Rechner hinweg: Es verlinkt die Laufzeitdaten eines Pools (Transcripts unter `projects/`, `history.jsonl` — also das, was `/resume` braucht) per Symlink aus dem Pool-Config-Verzeichnis nach `~/claude-projects/pool/<pool>`, sprich in dein claude-projects-Repo. Damit wandern die Unterhaltungen beim Repo-Sync mit auf den nächsten Mac. Gesperrt, solange ein Projekt des Pools läuft (sonst würde das Transkript der laufenden Session ersetzt). Das ist der Teil, der auf deinem Setup-Konstrukt (`~/claude-projects` als Sync-Repo) aufbaut und bei einem fremden Nutzer ins Leere zeigt.

History-Squash ist vorgemerkt — den mache ich beim eigentlichen GitHub-Push (frisches Repo, Ist-Stand als Initial-Commit; dein Gitea behält die volle History).


hm -- hilft es das configurierbar zu machen
vor allem wo?


Ja, das hilft — die Mechanik ist generisch (Laufzeitdaten des Pools in einen gesyncten Ordner verlegen, Symlinks im Config-Verzeichnis zurücklassen), nur das Ziel `~/claude-projects/pool` ist dein persönliches Konstrukt. Konfigurierbar wird daraus: „verlinke Pool-Sessions in meinen Sync-Ordner" — egal ob der per git, Syncthing oder iCloud wandert.

Ort: `~/.config/ai-control/settings.json`, neben `claudeCommand` und `syncOnSessionEnd` — z. B. `poolSyncDir`. Ungesetzt = Feature aus (Button/Command in der UI ausgeblendet, statt ins Leere zu zeigen); bei dir: `"poolSyncDir": "~/claude-projects/pool"`. Dazu die Zeile in der README-Settings-Tabelle.

Sag Go, dann baue ich das so um (inkl. Eintrag in deiner settings.json).


moment
was passiert, wenn jemand keinen sync will?


Nichts — das ist dann der Normalzustand: `poolSyncDir` ungesetzt, die Laufzeitdaten (Transcripts, `history.jsonl`) bleiben einfach im Pool-Config-Verzeichnis unter `~/.config/ai-control/pools/<pool>`, `/resume` funktioniert lokal wie gehabt. Der Verlinken-Button erscheint gar nicht erst, und der Session-End-Sync ist ohnehin ein separates Opt-in (`syncOnSessionEnd`, Default aus). Sync ist in beiden Fällen reine Zusatzfunktion — wer nichts konfiguriert, hat eine komplett lokale App.


opk -- dann mach das


grep -n "link_pool_runtime\|pool_runtime_target\|fn sync_setting\|syncOnSessionEnd" src-tauri/src/lib.rs | head -12

Locate pool runtime linking code


213:    .and_then(|v| v["syncOnSessionEnd"].as_bool())
240:  v["syncOnSessionEnd"] = serde_json::json!(enabled);
1216:fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
1269:  link_pool_runtime_in(paths, &id)?;
1282:  link_pool_runtime_in(paths, &id)?;
1790:fn sync_setting() -> Result<bool, String> {
1802:fn link_pool_runtime(pool: String) -> Result<(), String> {
1807:  link_pool_runtime_in(&paths, &pool)
1984:      link_pool_runtime,


1180	  dir: &PathBuf,
1181	  extra: serde_json::Value,
1182	) -> Result<(), String> {
1183	  let mut settings = serde_json::json!({
1184	    "promptSuggestionEnabled": false,
1185	    "awaySummaryEnabled": false,
1186	  });
1187	  let base = settings.as_object_mut().unwrap();
1188	  if let Some(obj) = extra.as_object() {
1189	    for (k, v) in obj {
1190	      base.insert(k.clone(), v.clone());
1191	    }
1192	  }
1193	  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
1194	  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
1195	  fs::write(dir.join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;
1196	  let claude_md = dir.join("CLAUDE.md");
1197	  if !claude_md.is_file() {
1198	    fs::write(&claude_md, "").map_err(|e| e.to_string())?;
1199	  }
1200	  Ok(())
1201	}
1202
1203	/// Runtime, die pro Pool ins synced Repo gelinkt wird: Transkripte, Todos,
1204	/// Prompt-Historie. (name, ist_ordner)
1205	const SYNCED_RUNTIME: [(&str, bool); 3] =
1206	  [("projects", true), ("todos", true), ("history.jsonl", false)];
1207
1208	/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo.
1209	fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf {
1210	  paths.projects_dir().join("pool").join(pool)
1211	}
1212
1213	/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das
1214	/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.
1215	/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
1216	fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
1217	  check_name(pool)?;
1218	  let src = paths.pool_dir(pool);
1219	  let data = pool_data_dir(paths, pool);
1220	  for (name, is_dir) in SYNCED_RUNTIME {
1221	    let target = data.join(name);
1222	    if is_dir {
1223	      fs::create_dir_all(&target).map_err(|e| e.to_string())?;
1224	    } else {
1225	      fs::create_dir_all(&data).map_err(|e| e.to_string())?;
1226	      if !target.exists() {
1227	        fs::write(&target, "").map_err(|e| e.to_string())?;
1228	      }
1229	    }
1230	    let link = src.join(name);
1231	    if link.is_dir() && !link.is_symlink() {
1232	      fs::remove_dir_all(&link).map_err(|e| e.to_string())?;
1233	    } else if link.is_symlink() || link.exists() {
1234	      fs::remove_file(&link).map_err(|e| e.to_string())?;
1235	    }
1236	    std::os::unix::fs::symlink(&target, &link).map_err(|e| e.to_string())?;
1237	  }
1238	  Ok(())
1239	}
1240
1241	/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 nur mit
1242	/// allow_file), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
1243	/// Ohne Store und ohne allow_file bricht die Anlage ab, bevor etwas entsteht.
1244	/// Liefert die Pool-ID.
1245	fn create_apikey_pool_in(
1246	  paths: &Paths,
1247	  store: &dyn ApikeyStore,
1248	  name: &str,
1249	  key: &str,
1250	  allow_file: bool,
1251	) -> Result<String, String> {
1252	  let dir = check_new_pool(paths, name)?;
1253	  let key = key.trim();
1254	  if key.is_empty() {
1255	    return Err("leerer API-Key".into());
1256	  }
1257	  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
1258	  if store.set(&id, key).is_err() {
1259	    if !allow_file {
1260	      return Err(KEYCHAIN_UNAVAILABLE.into());
1261	    }
1262	    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
1263	  }
1264	  init_pool_config(
1265	    &dir,
1266	    serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),
1267	  )?;
1268	  write_pool_json(&dir, name, "apikey")?;
1269	  link_pool_runtime_in(paths, &id)?;
1270	  Ok(id)
1271	}
1272
1273	/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
1274	/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
1275	/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
1276	/// Liefert die Pool-ID.
1277	fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
1278	  let dir = check_new_pool(paths, name)?;
1279	  init_pool_config(&dir, serde_json::json!({}))?;
1280	  write_pool_json(&dir, name, "oauth")?;
1281	  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
1282	  link_pool_runtime_in(paths, &id)?;
1283	  Ok(id)
1284	}
1285
1286	/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
1287	/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
1288	fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
1289	  check_name(name)?;
1290	  let current = read_pool(paths, pool)?;
1291	  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
1292	    return Err(format!("Pool existiert bereits: {name}"));
1293	  }
1294	  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)

  dir: &PathBuf,
  extra: serde_json::Value,
) -> Result<(), String> {
  let mut settings = serde_json::json!({
    "promptSuggestionEnabled": false,
    "awaySummaryEnabled": false,
  });
  let base = settings.as_object_mut().unwrap();
  if let Some(obj) = extra.as_object() {
    for (k, v) in obj {
      base.insert(k.clone(), v.clone());
    }
  }
  fs::create_dir_all(dir).map_err(|e| e.to_string())?;
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;
  let claude_md = dir.join("CLAUDE.md");
  if !claude_md.is_file() {
    fs::write(&claude_md, "").map_err(|e| e.to_string())?;
  }
  Ok(())
}

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

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

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

/// Legt einen apikey-Pool an: Key in den Keychain/Keyring (Datei 0600 nur mit
/// allow_file), settings.json mit apiKeyHelper-Kette, CLAUDE.md, pool.json.
/// Ohne Store und ohne allow_file bricht die Anlage ab, bevor etwas entsteht.
/// Liefert die Pool-ID.
fn create_apikey_pool_in(
  paths: &Paths,
  store: &dyn ApikeyStore,
  name: &str,
  key: &str,
  allow_file: bool,
) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if store.set(&id, key).is_err() {
    if !allow_file {
      return Err(KEYCHAIN_UNAVAILABLE.into());
    }
    write_secret_file(&dir.join(APIKEY_FILE), &format!("{key}\n"))?;
  }
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

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

/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  if pool_names(paths)?.iter().any(|(id, n)| id != pool && n == name) {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)


grep -rn "link_pool_runtime\|linkRuntime\|link_runtime" src/ | head; grep -n "fn sync_setting\|fn set_sync_setting" -A 4 src-tauri/src/lib.rs | head -12

Find frontend usage of link_pool_runtime


1790:fn sync_setting() -> Result<bool, String> {
1791-  Ok(sync_on_session_end(&Paths::real()))
1792-}
1793-
1794-#[tauri::command]
1795:fn set_sync_setting(enabled: bool) -> Result<(), String> {
1796-  set_sync_on_session_end_in(&Paths::real(), enabled)
1797-}
1798-
1799-/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool —


grep -n "pool_data_dir" src-tauri/src/lib.rs

Find pool_data_dir usages


1209:fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf {
1219:  let data = pool_data_dir(paths, pool);
2619:      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
2620:      let target = pool_data_dir(&p, &id).join(name);


Edit

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

/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das
/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
  check_name(pool)?;
  let src = paths.pool_dir(pool);
  let data = pool_data_dir(paths, pool);

/// Sync-Ziel für Pool-Laufzeitdaten (settings.json: poolSyncDir).
/// Ungesetzt = Feature aus, alle Daten bleiben lokal im Pool-Ordner.
fn pool_sync_dir(paths: &Paths) -> Option<PathBuf> {
  fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .and_then(|v| v["poolSyncDir"].as_str().map(|d| expand_home(paths, d)))
}

/// Zielort der synced Runtime-Daten eines Pools unterhalb von poolSyncDir.
fn pool_data_dir(paths: &Paths, pool: &str) -> Result<PathBuf, String> {
  pool_sync_dir(paths)
    .map(|d| d.join(pool))
    .ok_or_else(|| "poolSyncDir ist nicht konfiguriert".to_string())
}

/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf den
/// konfigurierten Sync-Ordner. Die Symlinks sind maschinenlokal, die Daten
/// reisen über den Sync des Zielordners (z. B. git).
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
  check_name(pool)?;
  let src = paths.pool_dir(pool);
  let data = pool_data_dir(paths, pool)?;


The file /Users/marcus.hinz/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)

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

/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das
/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
  check_name(pool)?;
  let src = paths.pool_dir(pool);
  let data = pool_data_dir(paths, pool);

/// Sync-Ziel für Pool-Laufzeitdaten (settings.json: poolSyncDir).
/// Ungesetzt = Feature aus, alle Daten bleiben lokal im Pool-Ordner.
fn pool_sync_dir(paths: &Paths) -> Option<PathBuf> {
  fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .and_then(|v| v["poolSyncDir"].as_str().map(|d| expand_home(paths, d)))
}

/// Zielort der synced Runtime-Daten eines Pools unterhalb von poolSyncDir.
fn pool_data_dir(paths: &Paths, pool: &str) -> Result<PathBuf, String> {
  pool_sync_dir(paths)
    .map(|d| d.join(pool))
    .ok_or_else(|| "poolSyncDir ist nicht konfiguriert".to_string())
}

/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf den
/// konfigurierten Sync-Ordner. Die Symlinks sind maschinenlokal, die Daten
/// reisen über den Sync des Zielordners (z. B. git).
/// Vorhandene echte Inhalte werden verworfen (kein History-Erhalt — bewusst).
fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> {
  check_name(pool)?;
  let src = paths.pool_dir(pool);
  let data = pool_data_dir(paths, pool)?;

const SYNCED_RUNTIME: [(&str, bool); 3] =

[("projects", true), ("todos", true), ("history.jsonl", false)];

-/// Zielort der synced Runtime-Daten eines Pools im claude-projects-Repo.

-fn pool_data_dir(paths: &Paths, pool: &str) -> PathBuf {

+/// Sync-Ziel für Pool-Laufzeitdaten (settings.json: poolSyncDir).

+/// Ungesetzt = Feature aus, alle Daten bleiben lokal im Pool-Ordner.

+fn pool_sync_dir(paths: &Paths) -> Option<PathBuf> {

+    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())

+    .and_then(|v| v["poolSyncDir"].as_str().map(|d| expand_home(paths, d)))

-/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf das

-/// synced Repo. Die Symlinks sind maschinenlokal, die Daten reisen über git.

+/// Zielort der synced Runtime-Daten eines Pools unterhalb von poolSyncDir.

+fn pool_data_dir(paths: &Paths, pool: &str) -> Result<PathBuf, String> {

+    .ok_or_else(|| "poolSyncDir ist nicht konfiguriert".to_string())

+/// Ersetzt im Pool-Ordner projects/todos/history.jsonl durch Symlinks auf den

+/// konfigurierten Sync-Ordner. Die Symlinks sind maschinenlokal, die Daten

+/// reisen über den Sync des Zielordners (z. B. git).

-  let data = pool_data_dir(paths, pool);

+  let data = pool_data_dir(paths, pool)?;


  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

  write_pool_json(&dir, name, "apikey")?;
  if pool_sync_dir(paths).is_some() {
    link_pool_runtime_in(paths, &id)?;
  }
  Ok(id)
}


  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

  write_pool_json(&dir, name, "apikey")?;
  if pool_sync_dir(paths).is_some() {
    link_pool_runtime_in(paths, &id)?;
  }
  Ok(id)
}

serde_json::json!({ "apiKeyHelper": apikey_helper_command(&dir, &id) }),


  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if pool_sync_dir(paths).is_some() {
    link_pool_runtime_in(paths, &id)?;
  }
  Ok(id)
}


  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  if pool_sync_dir(paths).is_some() {
    link_pool_runtime_in(paths, &id)?;
  }
  Ok(id)
}


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup" PATH="$HOME/tools/.cargo/bin:$PATH"; cargo test --manifest-path src-tauri/Cargo.toml 2>&1 | grep -E "^error|FAILED|panicked at|test result" -A 3 | head -40

Run tests to find affected linking tests


error[E0599]: no method named `join` found for enum `Result<T, E>` in the current scope
    --> src/lib.rs:2635:72
     |
2635 |       assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
--
error[E0599]: no method named `join` found for enum `Result<T, E>` in the current scope
    --> src/lib.rs:2636:43
     |
2636 |       let target = pool_data_dir(&p, &id).join(name);
--
error: could not compile `ai-control` (lib test) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...


2600
2601	  /// Store-Pool: hasCredentials kommt aus dem Keychain-Eintrag, ohne Datei.
2602	  #[test]
2603	  fn pool_key_status_aus_store() {
2604	    let p = tmp_paths();
2605	    let store = map_store();
2606	    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
2607	    assert!(!p.pool_dir(&kunde).join(APIKEY_FILE).exists());
2608	    let pools = list_pools_in(&p, &store).unwrap();
2609	    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
2610	    store.delete(&kunde).unwrap();
2611	    let pools = list_pools_in(&p, &store).unwrap();
2612	    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
2613	  }
2614
2615	  #[test]
2616	  fn pool_loeschen_loest_zuordnungen() {
2617	    let p = tmp_paths();
2618	    let store = map_store();
2619	    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
2620	    create_project_in(&p, "proj").unwrap();
2621	    assign_pool_in(&p, "proj", &kunde).unwrap();
2622	    delete_pool_in(&p, &store, &kunde).unwrap();
2623	    assert!(!p.pool_dir(&kunde).exists());
2624	    assert!(!project_config_path(&p, "proj").unwrap().exists());
2625	  }
2626
2627	  #[test]
2628	  fn pool_anlegen_verlinkt_synced_runtime() {
2629	    let p = tmp_paths();
2630	    let id = make_oauth_pool(&p, "privat");
2631	    let pooldir = p.pool_dir(&id);
2632	    for (name, is_dir) in SYNCED_RUNTIME {
2633	      let link = pooldir.join(name);
2634	      assert!(link.is_symlink(), "{name} sollte Symlink sein");
2635	      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
2636	      let target = pool_data_dir(&p, &id).join(name);
2637	      assert_eq!(target.is_dir(), is_dir);
2638	      assert!(target.exists());
2639	    }
2640	    // Zieldaten liegen im claude-projects-Repo unter pool/<id>/
2641	    assert!(p.projects_dir().join("pool").join(&id).is_dir());
2642	  }
2643
2644	  #[test]
2645	  fn sync_optin_default_aus_und_umschaltbar() {
2646	    let p = tmp_paths();
2647	    assert!(!sync_on_session_end(&p)); // default: kein Sync ohne Zustimmung
2648	    set_sync_on_session_end_in(&p, true).unwrap();
2649	    assert!(sync_on_session_end(&p));
2650	    set_sync_on_session_end_in(&p, false).unwrap();
2651	    assert!(!sync_on_session_end(&p));
2652	  }
2653
2654	  #[test]

  /// Store-Pool: hasCredentials kommt aus dem Keychain-Eintrag, ohne Datei.
  #[test]
  fn pool_key_status_aus_store() {
    let p = tmp_paths();
    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    assert!(!p.pool_dir(&kunde).join(APIKEY_FILE).exists());
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    store.delete(&kunde).unwrap();
    let pools = list_pools_in(&p, &store).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

  #[test]
  fn pool_loeschen_loest_zuordnungen() {
    let p = tmp_paths();
    let store = map_store();
    let kunde = make_apikey_pool(&p, &store, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &store, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!project_config_path(&p, "proj").unwrap().exists());
  }

  #[test]
  fn pool_anlegen_verlinkt_synced_runtime() {
    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    let pooldir = p.pool_dir(&id);
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
      let target = pool_data_dir(&p, &id).join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<id>/
    assert!(p.projects_dir().join("pool").join(&id).is_dir());
  }

  #[test]
  fn sync_optin_default_aus_und_umschaltbar() {
    let p = tmp_paths();
    assert!(!sync_on_session_end(&p)); // default: kein Sync ohne Zustimmung
    set_sync_on_session_end_in(&p, true).unwrap();
    assert!(sync_on_session_end(&p));
    set_sync_on_session_end_in(&p, false).unwrap();
    assert!(!sync_on_session_end(&p));
  }

  #[test]