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)

const APIKEY_FILE: &str = "apikey";
const POOL_FILE: &str = "pool.json";
const PROJECT_FILE: &str = "ai-control.json";

/// Wurzelpfade; in Tests mit temporärem home instanziierbar.
struct Paths {
  home: PathBuf,
}

impl Paths {
  fn real() -> Self {
    Paths {
      home: PathBuf::from(std::env::var("HOME").expect("HOME nicht gesetzt")),
    }
  }

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

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

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

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

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

const APIKEY_FILE: &str = "apikey";
const POOL_FILE: &str = "pool.json";
const PROJECT_FILE: &str = "ai-control.json";
const PROJECTS_FILE: &str = "projects.json";

/// Wurzelpfade; in Tests mit temporärem home instanziierbar.
pub(crate) struct Paths {
  home: PathBuf,
}

impl Paths {
  pub(crate) fn real() -> Self {
    Paths {
      home: PathBuf::from(std::env::var("HOME").expect("HOME nicht gesetzt")),
    }
  }

  /// Default-Root: Alt-Layout (Discovery ohne Registry) und Ablageort neuer
  /// Projekte ohne gewählten Zielordner.
  fn projects_dir(&self) -> PathBuf {
    self.home.join("claude-projects")
  }

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

  fn projects_file(&self) -> PathBuf {
    self.config_dir().join(PROJECTS_FILE)
  }

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

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

// ---------- Projekt-Registry ----------

/// Pfad unterhalb von Home als "~/…" schreiben — Registry-Einträge im
/// Home-Bereich bleiben damit maschinenübergreifend stabil.
fn contract_home(paths: &Paths, p: &std::path::Path) -> String {
  match p.strip_prefix(&paths.home) {
    Ok(rest) => format!("~/{}", rest.display()),
    Err(_) => p.display().to_string(),
  }
}

/// Registry Name → Projektordner. Ohne projects.json gilt das Alt-Layout:
/// Unterordner von ~/claude-projects mit .claude-Ordner.
fn load_registry(paths: &Paths) -> Result<std::collections::BTreeMap<String, PathBuf>, String> {
  let file = paths.projects_file();
  if file.is_file() {
    let raw = fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?;
    let map: std::collections::BTreeMap<String, String> =
      serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", file.display()))?;
    return Ok(
      map
        .into_iter()
        .map(|(name, p)| {
          let dir = expand_home(paths, &p);
          (name, dir)
        })
        .collect(),
    );
  }
  let mut map = std::collections::BTreeMap::new();
  let Ok(entries) = fs::read_dir(paths.projects_dir()) else {
    return Ok(map); // kein Alt-Layout → keine Projekte
  };
  for entry in entries {
    let entry = entry.map_err(|e| e.to_string())?;
    if entry.path().join(".claude").is_dir() {
      map.insert(entry.file_name().to_string_lossy().into_owned(), entry.path());
    }
  }
  Ok(map)
}

fn save_registry(
  paths: &Paths,
  reg: &std::collections::BTreeMap<String, PathBuf>,
) -> Result<(), String> {
  let contracted: std::collections::BTreeMap<&String, String> =
    reg.iter().map(|(name, dir)| (name, contract_home(paths, dir))).collect();
  let raw = serde_json::to_string_pretty(&contracted).map_err(|e| e.to_string())?;
  fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?;
  let file = paths.projects_file();
  fs::write(&file, raw + "\n").map_err(|e| format!("{}: {e}", file.display()))
}

/// Ordner eines registrierten Projekts.
pub(crate) fn project_dir(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  load_registry(paths)?
    .remove(name)
    .ok_or_else(|| format!("Projekt nicht registriert: {name}"))
}

/// Nimmt ein Projekt in die Registry auf; materialisiert dabei auch die
/// per Alt-Layout gefundenen Einträge in projects.json.
fn register_project(paths: &Paths, name: &str, dir: &std::path::Path) -> Result<(), String> {
  let mut reg = load_registry(paths)?;
  if reg.contains_key(name) {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  reg.insert(name.to_string(), dir.to_path_buf());
  save_registry(paths, &reg)
}

/// Entfernt nur den Registry-Eintrag; der Projektordner bleibt.
fn unregister_project(paths: &Paths, name: &str) -> Result<(), String> {
  let mut reg = load_registry(paths)?;
  if reg.remove(name).is_none() {
    return Err(format!("Projekt nicht registriert: {name}"));
  }
  save_registry(paths, &reg)
}

fn project_config_path(paths: &Paths, project: &str) -> Result<PathBuf, String> {
  Ok(project_dir(paths, project)?.join(PROJECT_FILE))
}

+const PROJECTS_FILE: &str = "projects.json";

+  /// Default-Root: Alt-Layout (Discovery ohne Registry) und Ablageort neuer

+  /// Projekte ohne gewählten Zielordner.

+    self.config_dir().join(PROJECTS_FILE)

-  fn project_config(&self, project: &str) -> PathBuf {

-    self.projects_dir().join(project).join(PROJECT_FILE)

+/// Pfad unterhalb von Home als "~/…" schreiben — Registry-Einträge im

+/// Home-Bereich bleiben damit maschinenübergreifend stabil.

+fn contract_home(paths: &Paths, p: &std::path::Path) -> String {

+    Ok(rest) => format!("~/{}", rest.display()),

+/// Registry Name → Projektordner. Ohne projects.json gilt das Alt-Layout:

+/// Unterordner von ~/claude-projects mit .claude-Ordner.

+fn load_registry(paths: &Paths) -> Result<std::collections::BTreeMap<String, PathBuf>, String> {

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

+    let map: std::collections::BTreeMap<String, String> =

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

+          let dir = expand_home(paths, &p);

+  let mut map = std::collections::BTreeMap::new();

+  let Ok(entries) = fs::read_dir(paths.projects_dir()) else {

+    return Ok(map); // kein Alt-Layout → keine Projekte

+    let entry = entry.map_err(|e| e.to_string())?;

+    if entry.path().join(".claude").is_dir() {

+      map.insert(entry.file_name().to_string_lossy().into_owned(), entry.path());

+  reg: &std::collections::BTreeMap<String, PathBuf>,

+  let contracted: std::collections::BTreeMap<&String, String> =

+    reg.iter().map(|(name, dir)| (name, contract_home(paths, dir))).collect();

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

+  fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?;

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

+/// Ordner eines registrierten Projekts.

+pub(crate) fn project_dir(paths: &Paths, name: &str) -> Result<PathBuf, String> {

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

+/// Nimmt ein Projekt in die Registry auf; materialisiert dabei auch die

+/// per Alt-Layout gefundenen Einträge in projects.json.

+fn register_project(paths: &Paths, name: &str, dir: &std::path::Path) -> Result<(), String> {

+    return Err(format!("Projekt existiert bereits: {name}"));

+  reg.insert(name.to_string(), dir.to_path_buf());

+/// Entfernt nur den Registry-Eintrag; der Projektordner bleibt.

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

+    return Err(format!("Projekt nicht registriert: {name}"));

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

+  Ok(project_dir(paths, project)?.join(PROJECT_FILE))


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


Edit

fn is_git_repo(dir: &PathBuf) -> bool {
  dir.join(".git").exists()
}

/// Nächstliegendes Git-Repo (Ordner mit .git) ab `dir` aufwärts.
fn git_root(dir: &std::path::Path) -> Option<PathBuf> {
  dir
    .ancestors()
    .find(|a| a.join(".git").exists())
    .map(|a| a.to_path_buf())
}


fn is_git_repo(dir: &PathBuf) -> bool {
  dir.join(".git").exists()
}

/// Nächstliegendes Git-Repo (Ordner mit .git) ab `dir` aufwärts.
fn git_root(dir: &std::path::Path) -> Option<PathBuf> {
  dir
    .ancestors()
    .find(|a| a.join(".git").exists())
    .map(|a| a.to_path_buf())
}

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

-fn is_git_repo(dir: &PathBuf) -> bool {

+/// Nächstliegendes Git-Repo (Ordner mit .git) ab `dir` aufwärts.

+fn git_root(dir: &std::path::Path) -> Option<PathBuf> {

/// (Name, läuft) aller Projekte, sortiert — dieselbe Erkennung wie die


/// Committet/pusht das claude-projects-Repo nach einem Session-Ende über das
/// vorhandene git-sync (add/commit/pull --rebase/push, konfliktsicher).
fn sync_session_context(home: &PathBuf, project: &str) {
  let git_sync = home.join("claude-projects/pool/bin/git-sync");
  let repo = home.join("claude-projects");
  match Command::new(&git_sync)
    .arg(&repo)
    .arg(format!("session-end sync: {project}"))
    .output()
  {
    Ok(o) if o.status.success() => {}
    Ok(o) => eprintln!(
      "session-end sync {project}: {}",
      String::from_utf8_lossy(&o.stderr).trim()
    ),
    Err(e) => eprintln!("session-end sync {project}: git-sync nicht startbar: {e}"),
  }
}

/// Committet/pusht das Repo des Projekts nach einem Session-Ende:
/// add -A → commit → pull --rebase → push. Nichts zu committen beendet still;
/// jeder andere Fehler bricht die Kette mit Meldung ab.
fn sync_session_context(repo: &std::path::Path, project: &str) {
  let run = |args: &[&str]| {
    Command::new("git")
      .arg("-C")
      .arg(repo)
      .args(args)
      .output()
      .map_err(|e| format!("git {}: {e}", args.join(" ")))
  };
  let msg = format!("session-end sync: {project}");
  let steps: [&[&str]; 4] = [
    &["add", "-A"],
    &["commit", "-m", &msg],
    &["pull", "--rebase"],
    &["push"],
  ];
  for (i, args) in steps.iter().enumerate() {
    match run(args) {
      Ok(o) if o.status.success() => {}
      // commit ohne Änderungen: nichts zu syncen, kein Fehler.
      Ok(_) if i == 1 && run(&["diff", "--cached", "--quiet"]).is_ok_and(|d| d.status.success()) => {
        return;
      }
      Ok(o) => {
        eprintln!(
          "session-end sync {project}: git {} — {}",
          args.join(" "),
          String::from_utf8_lossy(&o.stderr).trim()
        );
        return;
      }
      Err(e) => {
        eprintln!("session-end sync {project}: {e}");
        return;
      }
    }
  }
}


/// Committet/pusht das claude-projects-Repo nach einem Session-Ende über das
/// vorhandene git-sync (add/commit/pull --rebase/push, konfliktsicher).
fn sync_session_context(home: &PathBuf, project: &str) {
  let git_sync = home.join("claude-projects/pool/bin/git-sync");
  let repo = home.join("claude-projects");
  match Command::new(&git_sync)
    .arg(&repo)
    .arg(format!("session-end sync: {project}"))
    .output()
  {
    Ok(o) if o.status.success() => {}
    Ok(o) => eprintln!(
      "session-end sync {project}: {}",
      String::from_utf8_lossy(&o.stderr).trim()
    ),
    Err(e) => eprintln!("session-end sync {project}: git-sync nicht startbar: {e}"),
  }
}

/// Committet/pusht das Repo des Projekts nach einem Session-Ende:
/// add -A → commit → pull --rebase → push. Nichts zu committen beendet still;
/// jeder andere Fehler bricht die Kette mit Meldung ab.
fn sync_session_context(repo: &std::path::Path, project: &str) {
  let run = |args: &[&str]| {
    Command::new("git")
      .arg("-C")
      .arg(repo)
      .args(args)
      .output()
      .map_err(|e| format!("git {}: {e}", args.join(" ")))
  };
  let msg = format!("session-end sync: {project}");
  let steps: [&[&str]; 4] = [
    &["add", "-A"],
    &["commit", "-m", &msg],
    &["pull", "--rebase"],
    &["push"],
  ];
  for (i, args) in steps.iter().enumerate() {
    match run(args) {
      Ok(o) if o.status.success() => {}
      // commit ohne Änderungen: nichts zu syncen, kein Fehler.
      Ok(_) if i == 1 && run(&["diff", "--cached", "--quiet"]).is_ok_and(|d| d.status.success()) => {
        return;
      }
      Ok(o) => {
        eprintln!(
          "session-end sync {project}: git {} — {}",
          args.join(" "),
          String::from_utf8_lossy(&o.stderr).trim()
        );
        return;
      }
      Err(e) => {
        eprintln!("session-end sync {project}: {e}");
        return;
      }
    }
  }
}

-/// Committet/pusht das claude-projects-Repo nach einem Session-Ende über das

-/// vorhandene git-sync (add/commit/pull --rebase/push, konfliktsicher).

-fn sync_session_context(home: &PathBuf, project: &str) {

-  let git_sync = home.join("claude-projects/pool/bin/git-sync");

-  let repo = home.join("claude-projects");

-    .arg(format!("session-end sync: {project}"))

-      "session-end sync {project}: {}",

-      String::from_utf8_lossy(&o.stderr).trim()

-    Err(e) => eprintln!("session-end sync {project}: git-sync nicht startbar: {e}"),

+/// Committet/pusht das Repo des Projekts nach einem Session-Ende:

+/// add -A → commit → pull --rebase → push. Nichts zu committen beendet still;

+/// jeder andere Fehler bricht die Kette mit Meldung ab.

+fn sync_session_context(repo: &std::path::Path, project: &str) {

+      .map_err(|e| format!("git {}: {e}", args.join(" ")))

+  let msg = format!("session-end sync: {project}");

+  for (i, args) in steps.iter().enumerate() {

+      Ok(o) if o.status.success() => {}

+      // commit ohne Änderungen: nichts zu syncen, kein Fehler.

+      Ok(_) if i == 1 && run(&["diff", "--cached", "--quiet"]).is_ok_and(|d| d.status.success()) => {

+          "session-end sync {project}: git {} — {}",

+          String::from_utf8_lossy(&o.stderr).trim()

+        eprintln!("session-end sync {project}: {e}");


      // Nur syncen, wenn zugestimmt UND ein Git-Repo da ist — der Tray
      // verlangt kein Repo, er nutzt es nur, falls vorhanden.
      if !ended.is_empty()
        && sync_on_session_end(&paths)
        && is_git_repo(&paths.projects_dir())
      {
        for project in ended {
          sync_session_context(&paths.home, project);
        }
      }

      // Nur syncen, wenn zugestimmt UND das Projekt in einem Git-Repo liegt —
      // der Tray verlangt kein Repo, er nutzt es nur, falls vorhanden.
      if !ended.is_empty() && sync_on_session_end(&paths) {
        for project in ended {
          if let Ok(dir) = project_dir(&paths, project) {
            if let Some(repo) = git_root(&dir) {
              sync_session_context(&repo, project);
            }
          }
        }
      }


      // Nur syncen, wenn zugestimmt UND ein Git-Repo da ist — der Tray
      // verlangt kein Repo, er nutzt es nur, falls vorhanden.
      if !ended.is_empty()
        && sync_on_session_end(&paths)
        && is_git_repo(&paths.projects_dir())
      {
        for project in ended {
          sync_session_context(&paths.home, project);
        }
      }

      // Nur syncen, wenn zugestimmt UND das Projekt in einem Git-Repo liegt —
      // der Tray verlangt kein Repo, er nutzt es nur, falls vorhanden.
      if !ended.is_empty() && sync_on_session_end(&paths) {
        for project in ended {
          if let Ok(dir) = project_dir(&paths, project) {
            if let Some(repo) = git_root(&dir) {
              sync_session_context(&repo, project);
            }
          }
        }
      }

-      // Nur syncen, wenn zugestimmt UND ein Git-Repo da ist — der Tray

-      // verlangt kein Repo, er nutzt es nur, falls vorhanden.

-        && is_git_repo(&paths.projects_dir())

+      // Nur syncen, wenn zugestimmt UND das Projekt in einem Git-Repo liegt —

+      // der Tray verlangt kein Repo, er nutzt es nur, falls vorhanden.

+      if !ended.is_empty() && sync_on_session_end(&paths) {

-          sync_session_context(&paths.home, project);

+          if let Ok(dir) = project_dir(&paths, project) {

+            if let Some(repo) = git_root(&dir) {

+              sync_session_context(&repo, project);

// Menü-Objekte gehören auf den Main-Thread.


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

fn list_projects_in(paths: &Paths) -> Result<Vec<Project>, String> {
  let mut projects = Vec::new();
  for (name, dir) in load_registry(paths)? {
    let cfg = read_project_config_in(paths, &name)?;
    projects.push(Project {
      running: is_running(&name),
      path: dir.to_string_lossy().into_owned(),
      pool: cfg.pool,
      terminal: cfg.terminal,
      name,
    });
  }
  Ok(projects)
}


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

fn list_projects_in(paths: &Paths) -> Result<Vec<Project>, String> {
  let mut projects = Vec::new();
  for (name, dir) in load_registry(paths)? {
    let cfg = read_project_config_in(paths, &name)?;
    projects.push(Project {
      running: is_running(&name),
      path: dir.to_string_lossy().into_owned(),
      pool: cfg.pool,
      terminal: cfg.terminal,
      name,
    });
  }
  Ok(projects)
}

-  let entries = fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())?;

-    let entry = entry.map_err(|e| e.to_string())?;

-    if !path.join(".claude").is_dir() {

-    let name = entry.file_name().to_string_lossy().into_owned();

+  for (name, dir) in load_registry(paths)? {

-      path: path.to_string_lossy().into_owned(),

+      path: dir.to_string_lossy().into_owned(),

-  projects.sort_by(|a, b| a.name.cmp(&b.name));


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

fn settings_path(paths: &Paths, name: &str) -> PathBuf {
  paths
    .projects_dir()
    .join(name)
    .join(".claude")
    .join("settings.json")
}

fn todo_hook_command(dir: &std::path::Path) -> String {
  format!(
    "jq -Rs '{{systemMessage: ., hookSpecificOutput:{{hookEventName:\"SessionStart\", additionalContext: .}}}}' {}",
    dir.join(TODO_FILE).display()
  )
}

fn settings_path(dir: &std::path::Path) -> PathBuf {
  dir.join(".claude").join("settings.json")
}


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

fn settings_path(paths: &Paths, name: &str) -> PathBuf {
  paths
    .projects_dir()
    .join(name)
    .join(".claude")
    .join("settings.json")
}

fn todo_hook_command(dir: &std::path::Path) -> String {
  format!(
    "jq -Rs '{{systemMessage: ., hookSpecificOutput:{{hookEventName:\"SessionStart\", additionalContext: .}}}}' {}",
    dir.join(TODO_FILE).display()
  )
}

fn settings_path(dir: &std::path::Path) -> PathBuf {
  dir.join(".claude").join("settings.json")
}

const TODO_FILE: &str = "OFFENE-PUNKTE.md";

const TODO_SKELETON: &str = "# Offene Punkte — bei jedem Start prüfen und abhaken\n\nKeine offenen Punkte.\n";

-fn todo_hook_command(paths: &Paths, name: &str) -> String {

+fn todo_hook_command(dir: &std::path::Path) -> String {

-    paths.projects_dir().join(name).join(TODO_FILE).display()

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

+fn settings_path(dir: &std::path::Path) -> PathBuf {

fn hook_is_todo(group: &serde_json::Value) -> bool {


fn todo_state_in(paths: &Paths, name: &str) -> Result<bool, String> {
  check_name(name)?;
  let sp = settings_path(paths, name);

fn todo_state_in(paths: &Paths, name: &str) -> Result<bool, String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);


fn todo_state_in(paths: &Paths, name: &str) -> Result<bool, String> {
  check_name(name)?;
  let sp = settings_path(paths, name);

fn todo_state_in(paths: &Paths, name: &str) -> Result<bool, String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);

+  let sp = settings_path(&project_dir(paths, name)?);


fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
  check_name(name)?;
  let sp = settings_path(paths, name);

fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = project_dir(paths, name)?;
  let sp = settings_path(&dir);


fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
  check_name(name)?;
  let sp = settings_path(paths, name);

fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = project_dir(paths, name)?;
  let sp = settings_path(&dir);

return Err(format!("settings.json fehlt: {}", sp.display()));


  session_start.retain(|g| !hook_is_todo(g));
  if enabled {
    session_start.insert(
      0,
      serde_json::json!({
        "hooks": [
          { "type": "command", "command": todo_hook_command(paths, name) }
        ]
      }),
    );
    let todo_path = paths.projects_dir().join(name).join(TODO_FILE);

  session_start.retain(|g| !hook_is_todo(g));
  if enabled {
    session_start.insert(
      0,
      serde_json::json!({
        "hooks": [
          { "type": "command", "command": todo_hook_command(&dir) }
        ]
      }),
    );
    let todo_path = dir.join(TODO_FILE);


  session_start.retain(|g| !hook_is_todo(g));
  if enabled {
    session_start.insert(
      0,
      serde_json::json!({
        "hooks": [
          { "type": "command", "command": todo_hook_command(paths, name) }
        ]
      }),
    );
    let todo_path = paths.projects_dir().join(name).join(TODO_FILE);

  session_start.retain(|g| !hook_is_todo(g));
  if enabled {
    session_start.insert(
      0,
      serde_json::json!({
        "hooks": [
          { "type": "command", "command": todo_hook_command(&dir) }
        ]
      }),
    );
    let todo_path = dir.join(TODO_FILE);

-          { "type": "command", "command": todo_hook_command(paths, name) }

+          { "type": "command", "command": todo_hook_command(&dir) }

-    let todo_path = paths.projects_dir().join(name).join(TODO_FILE);

+    let todo_path = dir.join(TODO_FILE);

.map_err(|e| format!("{}: {e}", todo_path.display()))?;


/// Legt ein Projekt nach dem Muster der bestehenden an: memory/, .gitignore
/// (Sentinel), .claude/settings.json (autoMemoryDirectory, Permissions,
/// Berechtigungen), ai-control.json mit Pool/Terminal-Config. Kein git init —
/// der Ordner ist Teil des einen claude-projects-Repos.
fn create_project_full_in(
  paths: &Paths,
  name: &str,
  pool: Option<&str>,
  work_dir: Option<&str>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  if let Some(pool) = pool {
    if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
      return Err(format!("Pool existiert nicht: {pool}"));
    }
  }
  // Arbeitsverzeichnis zuerst prüfen/anlegen — scheitert das, entsteht kein
  // halbes Projekt.
  if let Some(wd) = work_dir {
    let wd_path = expand_home(paths, wd);
    if create_work_dir {
      fs::create_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    } else if !wd_path.is_dir() {
      return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
    }
  }

  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?;
  fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?;

  let mut allow = vec![format!("Edit(~/claude-projects/{name}/**)")];
  let mut additional: Vec<String> = Vec::new();
  if let Some(wd) = work_dir {
    allow.insert(0, format!("Edit({wd}/**)"));
    additional.push(wd.to_string());
  }
  let settings = serde_json::json!({
    "autoMemoryDirectory": format!("~/claude-projects/{name}/memory"),
    "permissions": {
      "allow": allow,
      "additionalDirectories": additional,
    },
  });
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join(".claude").join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;

  let cfg = ProjectConfig { pool: pool.map(str::to_string), terminal };
  if cfg.pool.is_some() || !cfg.terminal.is_empty() {
    write_project_config_in(paths, name, &cfg)?;
  }
  if todo {
    set_todo_in(paths, name, true)?;
  }
  Ok(())
}

#[cfg(test)]
fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())
}

/// Legt ein Projekt nach dem Muster der bestehenden an: memory/, .gitignore
/// (Sentinel), .claude/settings.json (autoMemoryDirectory, Permissions,
/// Berechtigungen), ai-control.json mit Pool/Terminal-Config, Registry-Eintrag.
/// Ohne Zielordner landet das Projekt unter ~/claude-projects/<name>.
fn create_project_full_in(
  paths: &Paths,
  name: &str,
  dir: Option<&str>,
  pool: Option<&str>,
  work_dir: Option<&str>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  check_name(name)?;
  let dir = match dir {
    Some(d) => expand_home(paths, d),
    None => paths.projects_dir().join(name),
  };
  if load_registry(paths)?.contains_key(name) {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  if dir.exists() {
    return Err(format!("Ordner existiert bereits: {}", dir.display()));
  }
  if let Some(pool) = pool {
    if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
      return Err(format!("Pool existiert nicht: {pool}"));
    }
  }
  // Arbeitsverzeichnis zuerst prüfen/anlegen — scheitert das, entsteht kein
  // halbes Projekt.
  if let Some(wd) = work_dir {
    let wd_path = expand_home(paths, wd);
    if create_work_dir {
      fs::create_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    } else if !wd_path.is_dir() {
      return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
    }
  }

  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?;
  fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?;

  let contracted = contract_home(paths, &dir);
  let mut allow = vec![format!("Edit({contracted}/**)")];
  let mut additional: Vec<String> = Vec::new();
  if let Some(wd) = work_dir {
    allow.insert(0, format!("Edit({wd}/**)"));
    additional.push(wd.to_string());
  }
  let settings = serde_json::json!({
    "autoMemoryDirectory": format!("{contracted}/memory"),
    "permissions": {
      "allow": allow,
      "additionalDirectories": additional,
    },
  });
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join(".claude").join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;

  register_project(paths, name, &dir)?;
  let cfg = ProjectConfig { pool: pool.map(str::to_string), terminal };
  if cfg.pool.is_some() || !cfg.terminal.is_empty() {
    write_project_config_in(paths, name, &cfg)?;
  }
  if todo {
    set_todo_in(paths, name, true)?;
  }
  Ok(())
}

/// Nimmt einen bestehenden Ordner als Projekt auf; der Name ist der Ordnername.
fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> {
  let dir = expand_home(paths, path);
  if !dir.is_dir() {
    return Err(format!("Ordner nicht gefunden: {}", dir.display()));
  }
  let name = dir
    .file_name()
    .ok_or_else(|| format!("kein Ordnername: {}", dir.display()))?
    .to_string_lossy()
    .into_owned();
  check_name(&name)?;
  register_project(paths, &name, &dir)
}

#[cfg(test)]
fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  // Registrieren nur, wenn die Registry schon materialisiert ist — sonst
  // greift der Alt-Layout-Scan.
  if paths.projects_file().is_file() {
    register_project(paths, name, &dir)?;
  }
  Ok(())
}


/// Legt ein Projekt nach dem Muster der bestehenden an: memory/, .gitignore
/// (Sentinel), .claude/settings.json (autoMemoryDirectory, Permissions,
/// Berechtigungen), ai-control.json mit Pool/Terminal-Config. Kein git init —
/// der Ordner ist Teil des einen claude-projects-Repos.
fn create_project_full_in(
  paths: &Paths,
  name: &str,
  pool: Option<&str>,
  work_dir: Option<&str>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  if let Some(pool) = pool {
    if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
      return Err(format!("Pool existiert nicht: {pool}"));
    }
  }
  // Arbeitsverzeichnis zuerst prüfen/anlegen — scheitert das, entsteht kein
  // halbes Projekt.
  if let Some(wd) = work_dir {
    let wd_path = expand_home(paths, wd);
    if create_work_dir {
      fs::create_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    } else if !wd_path.is_dir() {
      return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
    }
  }

  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?;
  fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?;

  let mut allow = vec![format!("Edit(~/claude-projects/{name}/**)")];
  let mut additional: Vec<String> = Vec::new();
  if let Some(wd) = work_dir {
    allow.insert(0, format!("Edit({wd}/**)"));
    additional.push(wd.to_string());
  }
  let settings = serde_json::json!({
    "autoMemoryDirectory": format!("~/claude-projects/{name}/memory"),
    "permissions": {
      "allow": allow,
      "additionalDirectories": additional,
    },
  });
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join(".claude").join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;

  let cfg = ProjectConfig { pool: pool.map(str::to_string), terminal };
  if cfg.pool.is_some() || !cfg.terminal.is_empty() {
    write_project_config_in(paths, name, &cfg)?;
  }
  if todo {
    set_todo_in(paths, name, true)?;
  }
  Ok(())
}

#[cfg(test)]
fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())
}

/// Legt ein Projekt nach dem Muster der bestehenden an: memory/, .gitignore
/// (Sentinel), .claude/settings.json (autoMemoryDirectory, Permissions,
/// Berechtigungen), ai-control.json mit Pool/Terminal-Config, Registry-Eintrag.
/// Ohne Zielordner landet das Projekt unter ~/claude-projects/<name>.
fn create_project_full_in(
  paths: &Paths,
  name: &str,
  dir: Option<&str>,
  pool: Option<&str>,
  work_dir: Option<&str>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  check_name(name)?;
  let dir = match dir {
    Some(d) => expand_home(paths, d),
    None => paths.projects_dir().join(name),
  };
  if load_registry(paths)?.contains_key(name) {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  if dir.exists() {
    return Err(format!("Ordner existiert bereits: {}", dir.display()));
  }
  if let Some(pool) = pool {
    if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
      return Err(format!("Pool existiert nicht: {pool}"));
    }
  }
  // Arbeitsverzeichnis zuerst prüfen/anlegen — scheitert das, entsteht kein
  // halbes Projekt.
  if let Some(wd) = work_dir {
    let wd_path = expand_home(paths, wd);
    if create_work_dir {
      fs::create_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    } else if !wd_path.is_dir() {
      return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
    }
  }

  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?;
  fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?;

  let contracted = contract_home(paths, &dir);
  let mut allow = vec![format!("Edit({contracted}/**)")];
  let mut additional: Vec<String> = Vec::new();
  if let Some(wd) = work_dir {
    allow.insert(0, format!("Edit({wd}/**)"));
    additional.push(wd.to_string());
  }
  let settings = serde_json::json!({
    "autoMemoryDirectory": format!("{contracted}/memory"),
    "permissions": {
      "allow": allow,
      "additionalDirectories": additional,
    },
  });
  let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
  fs::write(dir.join(".claude").join("settings.json"), raw + "\n").map_err(|e| e.to_string())?;

  register_project(paths, name, &dir)?;
  let cfg = ProjectConfig { pool: pool.map(str::to_string), terminal };
  if cfg.pool.is_some() || !cfg.terminal.is_empty() {
    write_project_config_in(paths, name, &cfg)?;
  }
  if todo {
    set_todo_in(paths, name, true)?;
  }
  Ok(())
}

/// Nimmt einen bestehenden Ordner als Projekt auf; der Name ist der Ordnername.
fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> {
  let dir = expand_home(paths, path);
  if !dir.is_dir() {
    return Err(format!("Ordner nicht gefunden: {}", dir.display()));
  }
  let name = dir
    .file_name()
    .ok_or_else(|| format!("kein Ordnername: {}", dir.display()))?
    .to_string_lossy()
    .into_owned();
  check_name(&name)?;
  register_project(paths, &name, &dir)
}

#[cfg(test)]
fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if dir.exists() {
    return Err(format!("Projekt existiert bereits: {name}"));
  }
  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;
  // Registrieren nur, wenn die Registry schon materialisiert ist — sonst
  // greift der Alt-Layout-Scan.
  if paths.projects_file().is_file() {
    register_project(paths, name, &dir)?;
  }
  Ok(())
}

-/// Berechtigungen), ai-control.json mit Pool/Terminal-Config. Kein git init —

-/// der Ordner ist Teil des einen claude-projects-Repos.

+/// Berechtigungen), ai-control.json mit Pool/Terminal-Config, Registry-Eintrag.

+/// Ohne Zielordner landet das Projekt unter ~/claude-projects/<name>.

-  let dir = paths.projects_dir().join(name);

+    None => paths.projects_dir().join(name),

+  if load_registry(paths)?.contains_key(name) {

+    return Err(format!("Ordner existiert bereits: {}", dir.display()));

-  let mut allow = vec![format!("Edit(~/claude-projects/{name}/**)")];

+  let contracted = contract_home(paths, &dir);

+  let mut allow = vec![format!("Edit({contracted}/**)")];

-    "autoMemoryDirectory": format!("~/claude-projects/{name}/memory"),

+    "autoMemoryDirectory": format!("{contracted}/memory"),

+  register_project(paths, name, &dir)?;

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

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

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

+    .ok_or_else(|| format!("kein Ordnername: {}", dir.display()))?

-  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())

+  fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?;

+  // Registrieren nur, wenn die Registry schon materialisiert ist — sonst

+    register_project(paths, name, &dir)?;

/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.


/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<String>, String> {
  check_name(name)?;
  let sp = paths
    .projects_dir()
    .join(name)
    .join(".claude")
    .join("settings.json");

/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<String>, String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);


/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<String>, String> {
  check_name(name)?;
  let sp = paths
    .projects_dir()
    .join(name)
    .join(".claude")
    .join("settings.json");

/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<String>, String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);

+  let sp = settings_path(&project_dir(paths, name)?);


/// Löscht den Projektordner unter claude-projects; optional auch die
/// verknüpften Arbeitsordner. Bei laufender Session wird abgebrochen.
fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if !dir.is_dir() {
    return Err(format!("Projekt nicht gefunden: {name}"));
  }
  if delete_work_dirs {
    for wd in project_work_dirs_in(paths, name)? {
      let wd_path = expand_home(paths, &wd);
      fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    }
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

/// Löscht den Projektordner und den Registry-Eintrag; optional auch die
/// verknüpften Arbeitsordner. Bei laufender Session wird abgebrochen.
fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = project_dir(paths, name)?;
  if !dir.is_dir() {
    return Err(format!("Projekt nicht gefunden: {name}"));
  }
  if delete_work_dirs {
    for wd in project_work_dirs_in(paths, name)? {
      let wd_path = expand_home(paths, &wd);
      fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    }
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())?;
  // Vor der Materialisierung gab es keinen Eintrag zu entfernen.
  if paths.projects_file().is_file() {
    unregister_project(paths, name)?;
  }
  Ok(())
}


/// Löscht den Projektordner unter claude-projects; optional auch die
/// verknüpften Arbeitsordner. Bei laufender Session wird abgebrochen.
fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = paths.projects_dir().join(name);
  if !dir.is_dir() {
    return Err(format!("Projekt nicht gefunden: {name}"));
  }
  if delete_work_dirs {
    for wd in project_work_dirs_in(paths, name)? {
      let wd_path = expand_home(paths, &wd);
      fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    }
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())
}

/// Löscht den Projektordner und den Registry-Eintrag; optional auch die
/// verknüpften Arbeitsordner. Bei laufender Session wird abgebrochen.
fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Result<(), String> {
  check_name(name)?;
  let dir = project_dir(paths, name)?;
  if !dir.is_dir() {
    return Err(format!("Projekt nicht gefunden: {name}"));
  }
  if delete_work_dirs {
    for wd in project_work_dirs_in(paths, name)? {
      let wd_path = expand_home(paths, &wd);
      fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?;
    }
  }
  fs::remove_dir_all(&dir).map_err(|e| e.to_string())?;
  // Vor der Materialisierung gab es keinen Eintrag zu entfernen.
  if paths.projects_file().is_file() {
    unregister_project(paths, name)?;
  }
  Ok(())
}

-/// Löscht den Projektordner unter claude-projects; optional auch die

+/// Löscht den Projektordner und den Registry-Eintrag; optional auch die

-  let dir = paths.projects_dir().join(name);

-  fs::remove_dir_all(&dir).map_err(|e| e.to_string())

+  fs::remove_dir_all(&dir).map_err(|e| e.to_string())?;

+  // Vor der Materialisierung gab es keinen Eintrag zu entfernen.

fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {


fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
  let cfg_path = paths.project_config(project);
  if !cfg_path.is_file() {
    return Ok(ProjectConfig::default());
  }
  let raw =
    fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
}

fn write_project_config_in(
  paths: &Paths,
  project: &str,
  cfg: &ProjectConfig,
) -> Result<(), String> {
  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
  fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
}

fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
  let cfg_path = project_config_path(paths, project)?;
  if !cfg_path.is_file() {
    return Ok(ProjectConfig::default());
  }
  let raw =
    fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
}

fn write_project_config_in(
  paths: &Paths,
  project: &str,
  cfg: &ProjectConfig,
) -> Result<(), String> {
  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
  fs::write(project_config_path(paths, project)?, raw + "\n").map_err(|e| e.to_string())
}


fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
  let cfg_path = paths.project_config(project);
  if !cfg_path.is_file() {
    return Ok(ProjectConfig::default());
  }
  let raw =
    fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
}

fn write_project_config_in(
  paths: &Paths,
  project: &str,
  cfg: &ProjectConfig,
) -> Result<(), String> {
  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
  fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
}

fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
  let cfg_path = project_config_path(paths, project)?;
  if !cfg_path.is_file() {
    return Ok(ProjectConfig::default());
  }
  let raw =
    fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
  serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
}

fn write_project_config_in(
  paths: &Paths,
  project: &str,
  cfg: &ProjectConfig,
) -> Result<(), String> {
  let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
  fs::write(project_config_path(paths, project)?, raw + "\n").map_err(|e| e.to_string())
}

-  let cfg_path = paths.project_config(project);

+  let cfg_path = project_config_path(paths, project)?;

-  fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())

+  fs::write(project_config_path(paths, project)?, raw + "\n").map_err(|e| e.to_string())

fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {


  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = None;
  if cfg.terminal.is_empty() {
    let cfg_path = paths.project_config(project);
    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
  }
  write_project_config_in(paths, project, &cfg)

  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = None;
  if cfg.terminal.is_empty() {
    let cfg_path = project_config_path(paths, project)?;
    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
  }
  write_project_config_in(paths, project, &cfg)


  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = None;
  if cfg.terminal.is_empty() {
    let cfg_path = paths.project_config(project);
    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
  }
  write_project_config_in(paths, project, &cfg)

  let mut cfg = read_project_config_in(paths, project)?;
  cfg.pool = None;
  if cfg.terminal.is_empty() {
    let cfg_path = project_config_path(paths, project)?;
    return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
  }
  write_project_config_in(paths, project, &cfg)

-    let cfg_path = paths.project_config(project);

+    let cfg_path = project_config_path(paths, project)?;


      let name = format!("icon.{ext}");
      let dest = paths.projects_dir().join(project).join(&name);

      let name = format!("icon.{ext}");
      let dest = project_dir(paths, project)?.join(&name);


      let name = format!("icon.{ext}");
      let dest = paths.projects_dir().join(project).join(&name);

      let name = format!("icon.{ext}");
      let dest = project_dir(paths, project)?.join(&name);

-      let dest = paths.projects_dir().join(project).join(&name);

+      let dest = project_dir(paths, project)?.join(&name);

fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;


/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im Projektordner.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> PathBuf {
  if icon.starts_with('/') {
    PathBuf::from(icon)
  } else {
    paths.projects_dir().join(project).join(icon)
  }
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im Projektordner.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(project_dir(paths, project)?.join(icon))
  }
}


/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im Projektordner.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> PathBuf {
  if icon.starts_with('/') {
    PathBuf::from(icon)
  } else {
    paths.projects_dir().join(project).join(icon)
  }
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im Projektordner.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(project_dir(paths, project)?.join(icon))
  }
}

-fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> PathBuf {

+fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> Result<PathBuf, String> {

-    paths.projects_dir().join(project).join(icon)

+    Ok(project_dir(paths, project)?.join(icon))


  let path = resolve_icon_path(&paths, &project, &icon);
  let is_icns = path

  let path = resolve_icon_path(&paths, &project, &icon)?;
  let is_icns = path


  let path = resolve_icon_path(&paths, &project, &icon);
  let is_icns = path

  let path = resolve_icon_path(&paths, &project, &icon)?;
  let is_icns = path

let Some(icon) = read_project_config_in(&paths, &project)?.terminal.icon else {

-  let path = resolve_icon_path(&paths, &project, &icon);

+  let path = resolve_icon_path(&paths, &project, &icon)?;


  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    let src = resolve_icon_path(paths, project, &icon);
    let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));

  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    let src = resolve_icon_path(paths, project, &icon)?;
    let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));


  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    let src = resolve_icon_path(paths, project, &icon);
    let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));

  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    let src = resolve_icon_path(paths, project, &icon)?;
    let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));

-    let src = resolve_icon_path(paths, project, &icon);

+    let src = resolve_icon_path(paths, project, &icon)?;


        .icon
        .map(|i| {
          resolve_icon_path(&Paths::real(), &project, &i)
            .to_string_lossy()
            .into_owned()
        });

        .icon
        .map(|i| {
          resolve_icon_path(&Paths::real(), &project, &i)
            .expect("Projekt nicht registriert")
            .to_string_lossy()
            .into_owned()
        });


        .icon
        .map(|i| {
          resolve_icon_path(&Paths::real(), &project, &i)
            .to_string_lossy()
            .into_owned()
        });

        .icon
        .map(|i| {
          resolve_icon_path(&Paths::real(), &project, &i)
            .expect("Projekt nicht registriert")
            .to_string_lossy()
            .into_owned()
        });

+            .expect("Projekt nicht registriert")


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

/// Namen aller Projekte, die `pool` zugeordnet haben.
fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
  let mut users = Vec::new();
  for (name, dir) in load_registry(paths)? {
    let cfg_path = dir.join(PROJECT_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
    if cfg.pool.as_deref() == Some(pool) {
      users.push(name);
    }
  }
  Ok(users)
}


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

/// Namen aller Projekte, die `pool` zugeordnet haben.
fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String> {
  let mut users = Vec::new();
  for (name, dir) in load_registry(paths)? {
    let cfg_path = dir.join(PROJECT_FILE);
    if !cfg_path.is_file() {
      continue;
    }
    let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
    let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
    if cfg.pool.as_deref() == Some(pool) {
      users.push(name);
    }
  }
  Ok(users)
}

-  let entries = fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())?;

-    let entry = entry.map_err(|e| e.to_string())?;

-    let cfg_path = entry.path().join(PROJECT_FILE);

+  for (name, dir) in load_registry(paths)? {

+    let cfg_path = dir.join(PROJECT_FILE);

-      users.push(entry.file_name().to_string_lossy().into_owned());


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

  // kodierter Projektpfad → Projektname
  let mut names = HashMap::new();
  for (name, dir) in load_registry(paths)? {
    names.insert(encode_project_path(&dir.to_string_lossy()), name);
  }


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

  // kodierter Projektpfad → Projektname
  let mut names = HashMap::new();
  for (name, dir) in load_registry(paths)? {
    names.insert(encode_project_path(&dir.to_string_lossy()), name);
  }

-  for entry in fs::read_dir(paths.projects_dir()).map_err(|e| e.to_string())? {

-    if !entry.path().join(".claude").is_dir() {

-    let name = entry.file_name().to_string_lossy().into_owned();

-    names.insert(encode_project_path(&entry.path().to_string_lossy()), name);

+    names.insert(encode_project_path(&dir.to_string_lossy()), name);


#[tauri::command]
fn create_project_full(
  name: String,
  pool: Option<String>,
  work_dir: Option<String>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  create_project_full_in(
    &Paths::real(),
    &name,
    pool.as_deref(),
    work_dir.as_deref(),
    create_work_dir,
    terminal,
    todo,
  )
}

#[tauri::command]
fn create_project_full(
  name: String,
  dir: Option<String>,
  pool: Option<String>,
  work_dir: Option<String>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  create_project_full_in(
    &Paths::real(),
    &name,
    dir.as_deref(),
    pool.as_deref(),
    work_dir.as_deref(),
    create_work_dir,
    terminal,
    todo,
  )
}

/// Bestehenden Ordner als Projekt aufnehmen (nur Registry-Eintrag).
#[tauri::command]
fn add_project(path: String) -> Result<(), String> {
  add_project_in(&Paths::real(), &path)
}

/// Projekt aus der Registry nehmen; der Ordner bleibt unangetastet.
#[tauri::command]
fn remove_project(name: String) -> Result<(), String> {
  if is_running(&name) {
    return Err(format!("{name} läuft noch — erst beenden"));
  }
  let paths = Paths::real();
  // Alt-Layout erst materialisieren, sonst taucht das Projekt beim
  // nächsten Scan wieder auf.
  if !paths.projects_file().is_file() {
    save_registry(&paths, &load_registry(&paths)?)?;
  }
  unregister_project(&paths, &name)
}


#[tauri::command]
fn create_project_full(
  name: String,
  pool: Option<String>,
  work_dir: Option<String>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  create_project_full_in(
    &Paths::real(),
    &name,
    pool.as_deref(),
    work_dir.as_deref(),
    create_work_dir,
    terminal,
    todo,
  )
}

#[tauri::command]
fn create_project_full(
  name: String,
  dir: Option<String>,
  pool: Option<String>,
  work_dir: Option<String>,
  create_work_dir: bool,
  terminal: TerminalConfig,
  todo: bool,
) -> Result<(), String> {
  create_project_full_in(
    &Paths::real(),
    &name,
    dir.as_deref(),
    pool.as_deref(),
    work_dir.as_deref(),
    create_work_dir,
    terminal,
    todo,
  )
}

/// Bestehenden Ordner als Projekt aufnehmen (nur Registry-Eintrag).
#[tauri::command]
fn add_project(path: String) -> Result<(), String> {
  add_project_in(&Paths::real(), &path)
}

/// Projekt aus der Registry nehmen; der Ordner bleibt unangetastet.
#[tauri::command]
fn remove_project(name: String) -> Result<(), String> {
  if is_running(&name) {
    return Err(format!("{name} läuft noch — erst beenden"));
  }
  let paths = Paths::real();
  // Alt-Layout erst materialisieren, sonst taucht das Projekt beim
  // nächsten Scan wieder auf.
  if !paths.projects_file().is_file() {
    save_registry(&paths, &load_registry(&paths)?)?;
  }
  unregister_project(&paths, &name)
}

#[tauri::command]
fn create_project_full(
name: String,
+  dir: Option<String>,
pool: Option<String>,
work_dir: Option<String>,
create_work_dir: bool,

+/// Bestehenden Ordner als Projekt aufnehmen (nur Registry-Eintrag).

+fn add_project(path: String) -> Result<(), String> {

+  add_project_in(&Paths::real(), &path)

+/// Projekt aus der Registry nehmen; der Ordner bleibt unangetastet.

+fn remove_project(name: String) -> Result<(), String> {

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

+  // Alt-Layout erst materialisieren, sonst taucht das Projekt beim

+  if !paths.projects_file().is_file() {

+    save_registry(&paths, &load_registry(&paths)?)?;

fn todo_state(project: String) -> Result<bool, String> {


    .invoke_handler(tauri::generate_handler![
      list_projects,
      create_project_full,
      delete_project,

    .invoke_handler(tauri::generate_handler![
      list_projects,
      create_project_full,
      add_project,
      remove_project,
      delete_project,


    .invoke_handler(tauri::generate_handler![
      list_projects,
      create_project_full,
      delete_project,

    .invoke_handler(tauri::generate_handler![
      list_projects,
      create_project_full,
      add_project,
      remove_project,
      delete_project,


Jetzt terminal.rs — die zwei hartkodierten Pfade:


#[tauri::command]
pub fn open_terminal(project: String) -> Result<(), String> {
  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
  let dir = PathBuf::from(home).join("claude-projects").join(&project);
  if !dir.is_dir() {
    return Err(format!("Projektordner fehlt: {}", dir.display()));
  }

#[tauri::command]
pub fn open_terminal(project: String) -> Result<(), String> {
  let dir = crate::project_dir(&crate::Paths::real(), &project)?;
  if !dir.is_dir() {
    return Err(format!("Projektordner fehlt: {}", dir.display()));
  }


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

#[tauri::command]
pub fn open_terminal(project: String) -> Result<(), String> {
  let dir = crate::project_dir(&crate::Paths::real(), &project)?;
  if !dir.is_dir() {
    return Err(format!("Projektordner fehlt: {}", dir.display()));
  }

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

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

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

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

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

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

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

/// Holt das Terminal-Fenster eines laufenden Projekts in den Vordergrund:
/// aktiviert den Terminal-Prozess über seine PID. Aktivierung ist seit
/// macOS 14 kooperativ — die Tray-App tritt sie vorher ab.
pub fn focus_terminal(pid: u32) {
  use objc2::MainThreadMarker;
  use objc2_app_kit::{NSApplication, NSApplicationActivationOptions, NSRunningApplication};
  let Some(term) = NSRunningApplication::runningApplicationWithProcessIdentifier(pid as i32)
  else {
    eprintln!("Terminal-Prozess {pid} nicht gefunden");
    return;
  };
  let mtm = MainThreadMarker::new().expect("focus_terminal läuft nicht auf dem Main-Thread");
  NSApplication::sharedApplication(mtm).yieldActivationToApplication(&term);
  term.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows);
}

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

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

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

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

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

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

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

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

  let label = window.label().to_string();
  let app = window.app_handle().clone();

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

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

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

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

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

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

-  let home = std::env::var("HOME").map_err(|e| e.to_string())?;

-  let dir = PathBuf::from(home).join("claude-projects").join(&project);

+  let dir = crate::project_dir(&crate::Paths::real(), &project)?;


  let home = std::env::var("HOME").map_err(|e| e.to_string())?;
  let cwd = PathBuf::from(home).join("claude-projects").join(&project);

  let pty = native_pty_system()

  let cwd = crate::project_dir(&crate::Paths::real(), &project)?;

  let pty = native_pty_system()


  let cwd = crate::project_dir(&crate::Paths::real(), &project)?;

  let pty = native_pty_system()

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

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

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

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

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

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

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

/// Holt das Terminal-Fenster eines laufenden Projekts in den Vordergrund:
/// aktiviert den Terminal-Prozess über seine PID. Aktivierung ist seit
/// macOS 14 kooperativ — die Tray-App tritt sie vorher ab.
pub fn focus_terminal(pid: u32) {
  use objc2::MainThreadMarker;
  use objc2_app_kit::{NSApplication, NSApplicationActivationOptions, NSRunningApplication};
  let Some(term) = NSRunningApplication::runningApplicationWithProcessIdentifier(pid as i32)
  else {
    eprintln!("Terminal-Prozess {pid} nicht gefunden");
    return;
  };
  let mtm = MainThreadMarker::new().expect("focus_terminal läuft nicht auf dem Main-Thread");
  NSApplication::sharedApplication(mtm).yieldActivationToApplication(&term);
  term.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows);
}

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

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

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

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

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

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

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

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

  let label = window.label().to_string();
  let app = window.app_handle().clone();

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

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

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

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

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

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

-  let cwd = PathBuf::from(home).join("claude-projects").join(&project);

+  let cwd = crate::project_dir(&crate::Paths::real(), &project)?;


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup" PATH="$HOME/tools/.cargo/bin:$PATH"; cargo check --manifest-path /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml 2>&1 | grep -E "^(error|warning)" -A 6 | head -60

Type-check backend changes


warning: unused import: `std::path::PathBuf`
 --> src/terminal.rs:5:5
  |
5 | use std::path::PathBuf;
  |     ^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
--
warning: `ai-control` (lib) generated 1 warning (run `cargo fix --lib -p ai-control` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.09s


Edit

use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::Mutex;

use std::io::{Read, Write};
use std::sync::Mutex;


use std::io::{Read, Write};
use std::sync::Mutex;

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

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

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

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

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

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

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

/// Holt das Terminal-Fenster eines laufenden Projekts in den Vordergrund:
/// aktiviert den Terminal-Prozess über seine PID. Aktivierung ist seit
/// macOS 14 kooperativ — die Tray-App tritt sie vorher ab.
pub fn focus_terminal(pid: u32) {
  use objc2::MainThreadMarker;
  use objc2_app_kit::{NSApplication, NSApplicationActivationOptions, NSRunningApplication};
  let Some(term) = NSRunningApplication::runningApplicationWithProcessIdentifier(pid as i32)
  else {
    eprintln!("Terminal-Prozess {pid} nicht gefunden");
    return;
  };
  let mtm = MainThreadMarker::new().expect("focus_terminal läuft nicht auf dem Main-Thread");
  NSApplication::sharedApplication(mtm).yieldActivationToApplication(&term);
  term.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows);
}

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

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

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

/// Startet die PTY für das rufende Fenster: `claude-sync` im Projektordner.
/// zsh -i lädt die .zshrc (PATH, fnm); Pool, Sentinel und git-Sync macht
/// claude-sync selbst.
#[tauri::command]
pub fn term_start(
  window: tauri::WebviewWindow,
  terminals: State<Terminals>,
  project: String,
  rows: u16,
  cols: u16,
) -> Result<(), String> {
  let cwd = crate::project_dir(&crate::Paths::real(), &project)?;

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

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

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

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

  let label = window.label().to_string();
  let app = window.app_handle().clone();

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

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

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

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

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

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


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup" PATH="$HOME/tools/.cargo/bin:$PATH"; cargo test --manifest-path /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml 2>&1 | tail -30

Run backend test suite


 486 |   work_dir: Option<&str>,
     |   ----------------------
help: provide the argument
     |
2618 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), /* std::option::Option<&str> */, true, TerminalConfig::default(), false)
     |                                                                       ++++++++++++++++++++++++++++++++

error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2627:5
     |
2627 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
     |     ^^^^^^^^^^^^^^^^^^^^^^                                            ---- argument #5 of type `std::option::Option<&str>` is missing
     |
note: function defined here
    --> src/lib.rs:481:4
     |
 481 | fn create_project_full_in(
     |    ^^^^^^^^^^^^^^^^^^^^^^
...
 486 |   work_dir: Option<&str>,
     |   ----------------------
help: provide the argument
     |
2627 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), /* std::option::Option<&str> */, true, TerminalConfig::default(), false)
     |                                                                       ++++++++++++++++++++++++++++++++

Some errors have detailed explanations: E0061, E0599.
For more information about an error, try `rustc --explain E0061`.
error: could not compile `ai-control` (lib test) due to 15 previous errors
warning: build failed, waiting for other jobs to finish...


export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup" PATH="$HOME/tools/.cargo/bin:$PATH"; cargo test --manifest-path /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml 2>&1 | grep -E "^error" -A 3 | head -80

List all test compile errors


error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2194:5
     |
2194 |     create_project_full_in(
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2246:5
     |
2246 |     create_project_full_in(&p, "proj", None, None, false, TerminalConfig::default(), false)
--
error[E0061]: this function takes 1 argument but 2 arguments were supplied
    --> src/lib.rs:2258:27
     |
2258 |       &fs::read_to_string(settings_path(&p, "proj")).unwrap(),
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2283:5
     |
2283 |     create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false).unwrap();
--
error[E0599]: no method named `project_config` found for struct `Paths` in the current scope
    --> src/lib.rs:2287:16
     |
  22 | pub(crate) struct Paths {
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2301:7
     |
2301 |       create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false)
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2309:15
     |
2309 |     let err = create_project_full_in(
--
error[E0599]: no method named `project_config` found for struct `Paths` in the current scope
    --> src/lib.rs:2452:16
     |
  22 | pub(crate) struct Paths {
--
error[E0599]: no method named `project_config` found for struct `Paths` in the current scope
    --> src/lib.rs:2517:50
     |
  22 | pub(crate) struct Paths {
--
error[E0599]: no method named `project_config` found for struct `Paths` in the current scope
    --> src/lib.rs:2539:16
     |
  22 | pub(crate) struct Paths {
--
error[E0599]: no method named `project_config` found for struct `Paths` in the current scope
    --> src/lib.rs:2561:36
     |
  22 | pub(crate) struct Paths {
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2598:5
     |
2598 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2608:5
     |
2608 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2618:5
     |
2618 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
--
error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2627:5
     |
2627 |     create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
--
error: could not compile `ai-control` (lib test) due to 15 previous errors


Testanpassungen — Signaturen in den Tests nachziehen:


grep -n "create_project_full_in(\|settings_path(&p\|\.project_config(" /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs | sed -n '1,40p'

Locate all failing test call sites


410:  let sp = settings_path(&project_dir(paths, name)?);
481:fn create_project_full_in(
584:  let sp = settings_path(&project_dir(paths, name)?);
1429:  create_project_full_in(
2194:    create_project_full_in(
2246:    create_project_full_in(&p, "proj", None, None, false, TerminalConfig::default(), false)
2258:      &fs::read_to_string(settings_path(&p, "proj")).unwrap(),
2283:    create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false).unwrap();
2287:    assert!(!p.project_config("neu").is_file());
2301:      create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false)
2309:    let err = create_project_full_in(
2452:    assert!(!p.project_config("proj").exists());
2517:      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
2539:    assert!(!p.project_config("proj").exists());
2561:    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
2598:    create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
2608:    create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
2618:    create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
2627:    create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)


2190	  #[test]
2191	  fn projekt_wizard_scaffold() {
2192	    let p = tmp_paths();
2193	    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
2194	    create_project_full_in(
2195	      &p,
2196	      "neu",
2197	      Some(&kunde),
2198	      Some("~/projects/neu"),
2199	      true,
2200	      TerminalConfig {
2201	        theme: Some("dracula".into()),
2202	        icon: None,
2203	        title: Some("Neu".into()),
2204	      },
2205	      true,
2206	    )
2207	    .unwrap();
2208
2209	    let dir = p.projects_dir().join("neu");
2210	    assert!(dir.join("memory").is_dir());
2211	    assert_eq!(

  #[test]
  fn projekt_wizard_scaffold() {
    let p = tmp_paths();
    let kunde = make_apikey_pool(&p, &map_store(), "kunde", "sk-1");
    create_project_full_in(
      &p,
      "neu",
      Some(&kunde),
      Some("~/projects/neu"),
      true,
      TerminalConfig {
        theme: Some("dracula".into()),
        icon: None,
        title: Some("Neu".into()),
      },
      true,
    )
    .unwrap();

    let dir = p.projects_dir().join("neu");
    assert!(dir.join("memory").is_dir());
    assert_eq!(


Edit

    create_project_full_in(
      &p,
      "neu",
      Some(&kunde),
      Some("~/projects/neu"),
      true,

    create_project_full_in(
      &p,
      "neu",
      None,
      Some(&kunde),
      Some("~/projects/neu"),
      true,


    create_project_full_in(
      &p,
      "neu",
      Some(&kunde),
      Some("~/projects/neu"),
      true,

    create_project_full_in(
      &p,
      "neu",
      None,
      Some(&kunde),
      Some("~/projects/neu"),
      true,

create_project_full_in(
&p,
"neu",
+      None,
Some(&kunde),
Some("~/projects/neu"),
true,


create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)

create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)


The file /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

-    create_project_full_in(&p, "proj", None, Some("~/projects/proj"), true, TerminalConfig::default(), false)

+    create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)


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

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


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

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


create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false).unwrap();

create_project_full_in(&p, "neu", None, None, None, false, TerminalConfig::default(), false).unwrap();


-    create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false).unwrap();

+    create_project_full_in(&p, "neu", None, None, None, false, TerminalConfig::default(), false).unwrap();

// ohne Pool und Terminal-Config entsteht keine ai-control.json


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

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


-      create_project_full_in(&p, "neu", None, None, false, TerminalConfig::default(), false)

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


// doppelt aktivieren erzeugt keinen zweiten Hook

let settings: serde_json::Value = serde_json::from_str(

-      &fs::read_to_string(settings_path(&p, "proj")).unwrap(),

+      &fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap(),

let groups = settings["hooks"]["SessionStart"].as_array().unwrap();


-    assert!(!p.project_config("neu").is_file());

+    assert!(!project_config_path(&p, "neu").unwrap().is_file());


-    assert!(!p.project_config("proj").exists());

+    assert!(!project_config_path(&p, "proj").unwrap().exists());

-      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())

+      serde_json::from_str(&fs::read_to_string(project_config_path(&p, "proj").unwrap()).unwrap())

-    let raw = fs::read_to_string(p.project_config("proj")).unwrap();

+    let raw = fs::read_to_string(project_config_path(&p, "proj").unwrap()).unwrap();

let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();

assert_eq!(cfg, serde_json::json!({ "pool": kunde }));


Bash

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

Run test suite


error[E0061]: this function takes 8 arguments but 7 arguments were supplied
    --> src/lib.rs:2310:15
     |
--
error: could not compile `ai-control` (lib test) due to 1 previous error
warning: build failed, waiting for other jobs to finish...