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

fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
    return Err(format!("Pool existiert nicht: {pool}"));
  }
  let mut cfg = read_project_config_in(paths, project)?;
  // Icon-Datei zieht mit dem Pool um.
  if let (Some(icon), Some(old)) = (cfg.terminal.icon.as_deref(), cfg.pool.as_deref()) {
    if !icon.starts_with('/') && old != pool {
      let src = pool_icons_dir(paths, old).join(icon);
      if src.is_file() {
        let icons = pool_icons_dir(paths, pool);
        fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
        fs::rename(&src, icons.join(icon)).map_err(|e| format!("{}: {e}", src.display()))?;
      }
    }
  }
  cfg.pool = Some(pool.to_string());
  write_project_config_in(paths, project, &cfg)
}

+  if let (Some(icon), Some(old)) = (cfg.terminal.icon.as_deref(), cfg.pool.as_deref()) {

+    if !icon.starts_with('/') && old != pool {

+      let src = pool_icons_dir(paths, old).join(icon);

+        let icons = pool_icons_dir(paths, pool);

+        fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;

+        fs::rename(&src, icons.join(icon)).map_err(|e| format!("{}: {e}", src.display()))?;


ok  Ich hätte gedacht, es kommt zum Pool  es muss NUR gesyned werden, wenn der Pool gesynced wird und gehört eigentlich nicht in ein src- Repo


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


[Request interrupted by user]


STOP
das ist beim poolwecsel doof wir bräuchten für alle pools eine gemeinsames resourcen Verzeichninis (direkt unter .config/ai-conbtriol)


Edit

fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
    return Err(format!("Pool existiert nicht: {pool}"));
  }
  let mut cfg = read_project_config_in(paths, project)?;
  // Icon-Datei zieht mit dem Pool um.
  if let (Some(icon), Some(old)) = (cfg.terminal.icon.as_deref(), cfg.pool.as_deref()) {
    if !icon.starts_with('/') && old != pool {
      let src = pool_icons_dir(paths, old).join(icon);
      if src.is_file() {
        let icons = pool_icons_dir(paths, pool);
        fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
        fs::rename(&src, icons.join(icon)).map_err(|e| format!("{}: {e}", src.display()))?;
      }
    }
  }
  cfg.pool = Some(pool.to_string());
  write_project_config_in(paths, project, &cfg)
}

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


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

fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
    return Err(format!("Pool existiert nicht: {pool}"));
  }
  let mut cfg = read_project_config_in(paths, project)?;
  // Icon-Datei zieht mit dem Pool um.
  if let (Some(icon), Some(old)) = (cfg.terminal.icon.as_deref(), cfg.pool.as_deref()) {
    if !icon.starts_with('/') && old != pool {
      let src = pool_icons_dir(paths, old).join(icon);
      if src.is_file() {
        let icons = pool_icons_dir(paths, pool);
        fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
        fs::rename(&src, icons.join(icon)).map_err(|e| format!("{}: {e}", src.display()))?;
      }
    }
  }
  cfg.pool = Some(pool.to_string());
  write_project_config_in(paths, project, &cfg)
}

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

-  if let (Some(icon), Some(old)) = (cfg.terminal.icon.as_deref(), cfg.pool.as_deref()) {

-    if !icon.starts_with('/') && old != pool {

-      let src = pool_icons_dir(paths, old).join(icon);

-        let icons = pool_icons_dir(paths, pool);

-        fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;

-        fs::rename(&src, icons.join(icon)).map_err(|e| format!("{}: {e}", src.display()))?;


/// Icons-Ordner eines Pools: Icons syncen mit dem Pool und gehören nicht ins
/// Projekt-/Quell-Repo.
fn pool_icons_dir(paths: &Paths, pool: &str) -> PathBuf {
  paths.pool_dir(pool).join("icons")
}

fn set_terminal_config_in(
  paths: &Paths,
  project: &str,
  mut terminal: TerminalConfig,
) -> Result<(), String> {
  let mut cfg = read_project_config_in(paths, project)?;
  // Absolut gewählte Icons in den Icons-Ordner des Pools kopieren und als
  // Dateiname speichern.
  if let Some(icon) = terminal.icon.as_deref() {
    if icon.starts_with('/') {
      let pool = cfg
        .pool
        .as_deref()
        .ok_or_else(|| format!("{project}: erst Pool zuordnen, dann Icon setzen"))?;
      let src = PathBuf::from(icon);
      let ext = src
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png")
        .to_lowercase();
      let name = format!("{project}.{ext}");
      let icons = pool_icons_dir(paths, pool);
      fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
      let dest = icons.join(&name);
      if src != dest {
        fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
      }
      terminal.icon = Some(name);
    }
  }
  cfg.terminal = terminal;
  write_project_config_in(paths, project, &cfg)
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// Icons-Ordner des zugeordneten Pools.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    return Ok(PathBuf::from(icon));
  }
  let pool = read_project_config_in(paths, project)?
    .pool
    .ok_or_else(|| format!("{project}: Icon {icon} ohne Pool-Zuordnung nicht auflösbar"))?;
  Ok(pool_icons_dir(paths, &pool).join(icon))
}

fn set_terminal_config_in(
  paths: &Paths,
  project: &str,
  mut terminal: TerminalConfig,
) -> Result<(), String> {
  let mut cfg = read_project_config_in(paths, project)?;
  // Absolut gewählte Icons ins gemeinsame Icons-Verzeichnis kopieren und als
  // Dateiname speichern — Icons gehören zur App-Config, nicht ins Quell-Repo.
  if let Some(icon) = terminal.icon.as_deref() {
    if icon.starts_with('/') {
      let src = PathBuf::from(icon);
      let ext = src
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png")
        .to_lowercase();
      let name = format!("{project}.{ext}");
      let icons = paths.icons_dir();
      fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
      let dest = icons.join(&name);
      if src != dest {
        fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
      }
      terminal.icon = Some(name);
    }
  }
  cfg.terminal = terminal;
  write_project_config_in(paths, project, &cfg)
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// gemeinsamen Icons-Verzeichnis der App.
fn resolve_icon_path(paths: &Paths, _project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(paths.icons_dir().join(icon))
  }
}


/// Icons-Ordner eines Pools: Icons syncen mit dem Pool und gehören nicht ins
/// Projekt-/Quell-Repo.
fn pool_icons_dir(paths: &Paths, pool: &str) -> PathBuf {
  paths.pool_dir(pool).join("icons")
}

fn set_terminal_config_in(
  paths: &Paths,
  project: &str,
  mut terminal: TerminalConfig,
) -> Result<(), String> {
  let mut cfg = read_project_config_in(paths, project)?;
  // Absolut gewählte Icons in den Icons-Ordner des Pools kopieren und als
  // Dateiname speichern.
  if let Some(icon) = terminal.icon.as_deref() {
    if icon.starts_with('/') {
      let pool = cfg
        .pool
        .as_deref()
        .ok_or_else(|| format!("{project}: erst Pool zuordnen, dann Icon setzen"))?;
      let src = PathBuf::from(icon);
      let ext = src
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png")
        .to_lowercase();
      let name = format!("{project}.{ext}");
      let icons = pool_icons_dir(paths, pool);
      fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
      let dest = icons.join(&name);
      if src != dest {
        fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
      }
      terminal.icon = Some(name);
    }
  }
  cfg.terminal = terminal;
  write_project_config_in(paths, project, &cfg)
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// Icons-Ordner des zugeordneten Pools.
fn resolve_icon_path(paths: &Paths, project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    return Ok(PathBuf::from(icon));
  }
  let pool = read_project_config_in(paths, project)?
    .pool
    .ok_or_else(|| format!("{project}: Icon {icon} ohne Pool-Zuordnung nicht auflösbar"))?;
  Ok(pool_icons_dir(paths, &pool).join(icon))
}

fn set_terminal_config_in(
  paths: &Paths,
  project: &str,
  mut terminal: TerminalConfig,
) -> Result<(), String> {
  let mut cfg = read_project_config_in(paths, project)?;
  // Absolut gewählte Icons ins gemeinsame Icons-Verzeichnis kopieren und als
  // Dateiname speichern — Icons gehören zur App-Config, nicht ins Quell-Repo.
  if let Some(icon) = terminal.icon.as_deref() {
    if icon.starts_with('/') {
      let src = PathBuf::from(icon);
      let ext = src
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png")
        .to_lowercase();
      let name = format!("{project}.{ext}");
      let icons = paths.icons_dir();
      fs::create_dir_all(&icons).map_err(|e| format!("{}: {e}", icons.display()))?;
      let dest = icons.join(&name);
      if src != dest {
        fs::copy(&src, &dest).map_err(|e| format!("{}: {e}", src.display()))?;
      }
      terminal.icon = Some(name);
    }
  }
  cfg.terminal = terminal;
  write_project_config_in(paths, project, &cfg)
}

/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// gemeinsamen Icons-Verzeichnis der App.
fn resolve_icon_path(paths: &Paths, _project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(paths.icons_dir().join(icon))
  }
}

-/// Icons-Ordner eines Pools: Icons syncen mit dem Pool und gehören nicht ins

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

-  // Absolut gewählte Icons in den Icons-Ordner des Pools kopieren und als

+  // Absolut gewählte Icons ins gemeinsame Icons-Verzeichnis kopieren und als

+  // Dateiname speichern — Icons gehören zur App-Config, nicht ins Quell-Repo.

-        .ok_or_else(|| format!("{project}: erst Pool zuordnen, dann Icon setzen"))?;

-      let icons = pool_icons_dir(paths, pool);

-/// Icons-Ordner des zugeordneten Pools.

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

+/// gemeinsamen Icons-Verzeichnis der App.

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

-  let pool = read_project_config_in(paths, project)?

-    .ok_or_else(|| format!("{project}: Icon {icon} ohne Pool-Zuordnung nicht auflösbar"))?;

-  Ok(pool_icons_dir(paths, &pool).join(icon))

/// Icon eines Projekts als PNG-data-URL für die Übersicht; ICNS wird per


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

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

  /// Gemeinsames Icons-Verzeichnis aller Projekte — synct mit der App-Config,
  /// unabhängig von Pool-Zuordnung und Quell-Repos.
  fn icons_dir(&self) -> PathBuf {
    self.config_dir().join("icons")
  }


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

  /// Gemeinsames Icons-Verzeichnis aller Projekte — synct mit der App-Config,
  /// unabhängig von Pool-Zuordnung und Quell-Repos.
  fn icons_dir(&self) -> PathBuf {
    self.config_dir().join("icons")
  }

+  /// Gemeinsames Icons-Verzeichnis aller Projekte — synct mit der App-Config,

+  /// unabhängig von Pool-Zuordnung und Quell-Repos.

fn pool_dir(&self, pool: &str) -> PathBuf {


/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// gemeinsamen Icons-Verzeichnis der App.
fn resolve_icon_path(paths: &Paths, _project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(paths.icons_dir().join(icon))
  }
}

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


/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
/// gemeinsamen Icons-Verzeichnis der App.
fn resolve_icon_path(paths: &Paths, _project: &str, icon: &str) -> Result<PathBuf, String> {
  if icon.starts_with('/') {
    Ok(PathBuf::from(icon))
  } else {
    Ok(paths.icons_dir().join(icon))
  }
}

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

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

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


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

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


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

  let path = resolve_icon_path(&paths, &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, &icon);


        .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(), &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(), &i)
            .to_string_lossy()
            .into_owned()
        });

-          resolve_icon_path(&Paths::real(), &project, &i)

-            .expect("Projekt nicht registriert")

+          resolve_icon_path(&Paths::real(), &i)


STOP das ist beim poolwecsel doof wir bräuchten für alle pools eine gemeinsames resourcen Verzeichninis (direkt unter .config/ai-conbtriol)


  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 out = Command::new("sips")
      .args(["-s", "format", "png", "-z", "36", "36"])
      .arg(&src)
      .arg("--out")
      .arg(&tmp)
      .output()
      .map_err(|e| format!("sips: {e}"))?;
    if !out.status.success() {
      return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
    }
    let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
    let _ = fs::remove_file(&tmp);
    let img = tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())?;
    let (iw, ih) = (img.width() as usize, img.height() as usize);
    let rgba = img.rgba();
    for y in 0..ih.min(H) {
      for x in 0..iw.min(H) {
        let src_i = (y * iw + x) * 4;
        let dst_i = (y * W + DOT_W + x) * 4;
        canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);
      }
    }
  }
  Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
}

  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    // Nicht ladbares Icon blockiert den Tray nicht (Muster set_dock_icon):
    // Meldung, Eintrag erscheint ohne Projekt-Icon.
    match project_icon_rgba_36(paths, project, &icon) {
      Ok(img) => {
        let (iw, ih) = (img.width() as usize, img.height() as usize);
        let rgba = img.rgba();
        for y in 0..ih.min(H) {
          for x in 0..iw.min(H) {
            let src_i = (y * iw + x) * 4;
            let dst_i = (y * W + DOT_W + x) * 4;
            canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);
          }
        }
      }
      Err(e) => eprintln!("Tray-Icon {project}: {e}"),
    }
  }
  Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
}

/// Projekt-Icon als 36×36-RGBA (sips konvertiert, auch ICNS).
fn project_icon_rgba_36(
  paths: &Paths,
  project: &str,
  icon: &str,
) -> Result<tauri::image::Image<'static>, String> {
  let src = resolve_icon_path(paths, icon);
  let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));
  let out = Command::new("sips")
    .args(["-s", "format", "png", "-z", "36", "36"])
    .arg(&src)
    .arg("--out")
    .arg(&tmp)
    .output()
    .map_err(|e| format!("sips: {e}"))?;
  if !out.status.success() {
    return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
  }
  let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
  let _ = fs::remove_file(&tmp);
  tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())
}


  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 out = Command::new("sips")
      .args(["-s", "format", "png", "-z", "36", "36"])
      .arg(&src)
      .arg("--out")
      .arg(&tmp)
      .output()
      .map_err(|e| format!("sips: {e}"))?;
    if !out.status.success() {
      return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
    }
    let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
    let _ = fs::remove_file(&tmp);
    let img = tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())?;
    let (iw, ih) = (img.width() as usize, img.height() as usize);
    let rgba = img.rgba();
    for y in 0..ih.min(H) {
      for x in 0..iw.min(H) {
        let src_i = (y * iw + x) * 4;
        let dst_i = (y * W + DOT_W + x) * 4;
        canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);
      }
    }
  }
  Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
}

  if let Some(icon) = read_project_config_in(paths, project)?.terminal.icon {
    // Nicht ladbares Icon blockiert den Tray nicht (Muster set_dock_icon):
    // Meldung, Eintrag erscheint ohne Projekt-Icon.
    match project_icon_rgba_36(paths, project, &icon) {
      Ok(img) => {
        let (iw, ih) = (img.width() as usize, img.height() as usize);
        let rgba = img.rgba();
        for y in 0..ih.min(H) {
          for x in 0..iw.min(H) {
            let src_i = (y * iw + x) * 4;
            let dst_i = (y * W + DOT_W + x) * 4;
            canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);
          }
        }
      }
      Err(e) => eprintln!("Tray-Icon {project}: {e}"),
    }
  }
  Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
}

/// Projekt-Icon als 36×36-RGBA (sips konvertiert, auch ICNS).
fn project_icon_rgba_36(
  paths: &Paths,
  project: &str,
  icon: &str,
) -> Result<tauri::image::Image<'static>, String> {
  let src = resolve_icon_path(paths, icon);
  let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));
  let out = Command::new("sips")
    .args(["-s", "format", "png", "-z", "36", "36"])
    .arg(&src)
    .arg("--out")
    .arg(&tmp)
    .output()
    .map_err(|e| format!("sips: {e}"))?;
  if !out.status.success() {
    return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
  }
  let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
  let _ = fs::remove_file(&tmp);
  tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())
}

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

-    let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));

-      .map_err(|e| format!("sips: {e}"))?;

-      return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));

-    let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;

-    let img = tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())?;

-    let (iw, ih) = (img.width() as usize, img.height() as usize);

-        let dst_i = (y * W + DOT_W + x) * 4;

-        canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);

+    // Nicht ladbares Icon blockiert den Tray nicht (Muster set_dock_icon):

+    // Meldung, Eintrag erscheint ohne Projekt-Icon.

+    match project_icon_rgba_36(paths, project, &icon) {

+        let (iw, ih) = (img.width() as usize, img.height() as usize);

+            let src_i = (y * iw + x) * 4;

+            let dst_i = (y * W + DOT_W + x) * 4;

+            canvas[dst_i..dst_i + 4].copy_from_slice(&rgba[src_i..src_i + 4]);

+      Err(e) => eprintln!("Tray-Icon {project}: {e}"),

+/// Projekt-Icon als 36×36-RGBA (sips konvertiert, auch ICNS).

+) -> Result<tauri::image::Image<'static>, String> {

+  let src = resolve_icon_path(paths, icon);

+  let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));

+    .map_err(|e| format!("sips: {e}"))?;

+    return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));

+  let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;

+  tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())

/// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess.

pub(crate) fn terminal_config(project: &str) -> Result<TerminalConfig, String> {


Bash

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

Run tests after icon relocation


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

     Running unittests src/main.rs (src-tauri/target/debug/deps/ai_control-cb387f5a798b9c01)

running 0 tests
--
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests app_lib

running 0 tests
--
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


set -e
cd ~/claude-projects && git checkout -- ai-control/icon.png
ICONS=~/.config/ai-control/icons; mkdir -p "$ICONS"
# relative Icons: aus dem Projektordner ins gemeinsame Verzeichnis verschieben
for p in ai-control robotunits wittgenstein wuerth-plato; do
  mv ~/claude-projects/$p/icon.png "$ICONS/$p.png"
done
for p in crm-sync landing-page webshop; do
  mv ~/demo-projects/$p/icon.icns "$ICONS/$p.icns"
done
# absolute Icons: kopieren
cp /home/marcuh/projects/claude-app-builder-4-mac/icons/08-07.png "$ICONS/08-07.png"
cp /home/marcuh/projects/claude-app-builder-4-mac/icons/limbach.png "$ICONS/limbach.png"
cp ~/claude-projects/misc/icon-m.png "$ICONS/misc.png"
ls "$ICONS"

Restore deleted icon and move all project icons to shared icons dir


08-07.png
ai-control.png
crm-sync.icns
landing-page.icns
limbach.png
misc.png
robotunits.png
webshop.icns
wittgenstein.png
wuerth-plato.png
Shell cwd was reset to /home/marcuh/claude-projects/ai-control


set -e
update() { f="$1"; icon="$2"; tmp=$(mktemp); jq --arg i "$icon" '.terminal.icon = $i' "$f" > "$tmp" && mv "$tmp" "$f"; }
update ~/claude-projects/08-07/ai-control.json "08-07.png"
update ~/claude-projects/ai-control/ai-control.json "ai-control.png"
update ~/claude-projects/limbach/ai-control.json "limbach.png"
update ~/claude-projects/misc/ai-control.json "misc.png"
update ~/claude-projects/robotunits/ai-control.json "robotunits.png"
update ~/claude-projects/wittgenstein/ai-control.json "wittgenstein.png"
update ~/claude-projects/wuerth-plato/ai-control.json "wuerth-plato.png"
update ~/demo-projects/crm-sync/ai-control.json "crm-sync.icns"
update ~/demo-projects/landing-page/ai-control.json "landing-page.icns"
update ~/demo-projects/webshop/ai-control.json "webshop.icns"
grep -h icon ~/claude-projects/*/ai-control.json ~/demo-projects/*/ai-control.json

Point all project configs at the shared icons dir


    "icon": "limbach.png"
    "icon": "misc.png"
    "icon": "ai-control.png"
    "icon": "08-07.png",
    "icon": "robotunits.png"
    "icon": "wuerth-plato.png"
    "icon": "wittgenstein.png"
    "icon": "crm-sync.icns"
    "icon": "landing-page.icns"
    "icon": "webshop.icns"


cd /home/marcuh/projects/ai-control && ./build.sh 2>&1 | tail -2 && rm -rf ~/Applications/ai-control.app && cp -R src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ && echo DEPLOYED

Build and deploy icon relocation + crash fix


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


Fertig, gebaut und deployed — App neu starten.

- **Icons zentral:** gemeinsames Verzeichnis `~/.config/ai-control/icons/<projekt>.<ext>` für alle Projekte, pool-unabhängig (kein Umzugsproblem beim Pool-Wechsel). `set_terminal_config` kopiert neu gewählte Icons dorthin; Referenz in `ai-control.json` ist nur der Dateiname.
- **Crash-Fix:** ein nicht ladbares Icon blockiert den Tray-Aufbau nicht mehr — Meldung nach stderr, Eintrag erscheint ohne Projekt-Icon (gleiches Muster wie `set_dock_icon`).
- **Migration:** alle 10 Projekte umgestellt; das gelöschte `icon.png` von ai-control aus git wiederhergestellt und mit umgezogen. Die relativen Icons sind aus den Projekt-/Demo-Ordnern raus — im claude-projects-Repo entsteht dadurch beim nächsten Sync die passende Lösch-Änderung, genau im Sinn von „gehört nicht ins Quell-Repo".
- Tests grün (48/48).

Der Commit-Stapel wächst: Icons-Umbau, Crash-Fix, README (EN, Screenshots), LICENSE, CSS-Fix, `claudeCommand`. Commit/Push auf dein Go.


ok die app ist weg


ls -t ~/Library/Logs/DiagnosticReports/ 2>/dev/null | head -5; pgrep -fl ai-control | grep -v pgrep | head

Check crash reports and running instances


(Bash completed with no output)


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

Run app binary briefly to capture startup errors


(eval):1: command not found: timeout
exit: 0


~/Applications/ai-control.app/Contents/MacOS/ai-control > /tmp/ai-control-test.log 2>&1 & APP_PID=$!; sleep 5; if kill -0 $APP_PID 2>/dev/null; then echo "LÄUFT nach 5s"; kill $APP_PID; else echo "BEENDET"; fi; cat /tmp/ai-control-test.log | head -20

Start app, check survival after 5s, capture output


LÄUFT nach 5s


/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/ai-control.app && open ~/Applications/ai-control.app && sleep 2 && pgrep -fl ai-control | grep -v pgrep | head -3

Re-register app with Launch Services and launch it


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


Läuft wieder — PID 28760, per `open` regulär gestartet und in Launch Services neu registriert. Das Tray-Symbol sollte da sein, mit allen 10 Projekten samt Icons aus dem neuen zentralen Verzeichnis.

Falls „weg" bei dir hieß „per Spotlight nicht mehr findbar": genau das war die verlorene Registrierung; die ist jetzt erneuert. Damit das nach künftigen Deploys nicht wieder passiert, könnte build.sh die installierte Kopie nach dem Kopieren gleich mit registrieren — sag Bescheid, dann nehme ich das (plus den offenen Commit-Stapel) mit auf.


kannst du die dempirojekte löschen?


python3 - <<'EOF'
import json, os
p = os.path.expanduser("~/.config/ai-control/projects.json")
reg = json.load(open(p))
for name in ("crm-sync", "landing-page", "webshop"):
    reg.pop(name, None)
json.dump(reg, open(p, "w"), indent=2, sort_keys=True)
open(p, "a").write("\n")
print(json.dumps(reg, indent=2))
EOF
rm -rf ~/demo-projects ~/.config/ai-control/icons/crm-sync.icns ~/.config/ai-control/icons/landing-page.icns ~/.config/ai-control/icons/webshop.icns ~/.config/ai-control/projects.json.real

Remove demo projects from registry, folders and icons


Gelöscht: die drei Registry-Einträge (übrig sind deine 7 Projekte), `~/demo-projects` komplett, die drei Demo-Icons aus `~/.config/ai-control/icons/` und das nicht mehr benötigte Registry-Backup `projects.json.real`. Liste und Tray zeigen nach dem nächsten Refresh wieder nur deine Projekte.


also der add Folder Button macht keinenb Sinn
zu welchem Projekt soll der Ordner denn gehören?


Zu keinem — der Ordner **wird** das Projekt: „+ Folder" nimmt ein bestehendes Verzeichnis als neues Projekt in die Liste auf (Name = Ordnername, es entsteht nur der Registry-Eintrag). Das ist der Import-Weg für Ordner, die es schon gibt — z. B. ein ausgechecktes Repo —, im Gegensatz zu „+ Project", das einen frischen Ordner mit Scaffolding anlegt.

Dass du ihn als „Ordner zu einem Projekt hinzufügen" gelesen hast, zeigt: das Label ist missverständlich — es klingt nach Arbeitsordner/additionalDirectories. Treffender wäre z. B. „Import project…" / „Projekt importieren…".


genau -- dann sind wir uns einig
jetzt fehlt mir niur noch die Möglichkeit, bei projekten zusätzlioch Ordner zu erfassen, die in die Settings eingetragen werden


Zwei Dinge also: „+ Folder" wird zu „Projekt importieren", und Projekte bekommen nachträglich erfassbare Arbeitsordner (settings.json: `additionalDirectories` + Edit-Permission). Backend zuerst:


[Request interrupted by user]


nicht addtional
wir hatten die - zumindest auch - unter permissions


Genau das ist geplant — beides, wie im Wizard: der Ordner landet in `permissions.additionalDirectories` **und** als `Edit(<ordner>/**)` in `permissions.allow`; beim Entfernen fliegen beide Einträge raus. Ich setze um.


Edit

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

/// Trägt einen Arbeitsordner in die Projekt-settings.json ein — wie im
/// Wizard: permissions.additionalDirectories + Edit-Permission. Legt die
/// Datei an, wenn sie fehlt (importierte Projekte).
fn add_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
  check_name(name)?;
  let wd_path = expand_home(paths, dir);
  if !wd_path.is_dir() {
    return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
  }
  let dir = contract_home(paths, &wd_path);
  let sp = settings_path(&project_dir(paths, name)?);
  let mut v: serde_json::Value = if sp.is_file() {
    let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?
  } else {
    serde_json::json!({})
  };
  let root = v.as_object_mut().ok_or("settings.json ist kein Objekt")?;
  let perms = root
    .entry("permissions")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
    .ok_or("permissions ist kein Objekt")?;
  let dirs = perms
    .entry("additionalDirectories")
    .or_insert_with(|| serde_json::json!([]))
    .as_array_mut()
    .ok_or("additionalDirectories ist kein Array")?;
  if dirs.iter().any(|d| d.as_str() == Some(&dir)) {
    return Err(format!("schon eingetragen: {dir}"));
  }
  dirs.push(serde_json::json!(dir));
  let allow = perms
    .entry("allow")
    .or_insert_with(|| serde_json::json!([]))
    .as_array_mut()
    .ok_or("allow ist kein Array")?;
  allow.insert(0, serde_json::json!(format!("Edit({dir}/**)")));
  let parent = sp.parent().ok_or("settings.json ohne Elternordner")?;
  fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}

/// Nimmt einen Arbeitsordner wieder raus: additionalDirectories-Eintrag und
/// zugehörige Edit-Permission. Der Ordner selbst bleibt.
fn remove_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);
  let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
  let mut v: serde_json::Value =
    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?;
  let perms = v["permissions"]
    .as_object_mut()
    .ok_or("permissions ist kein Objekt")?;
  let dirs = perms["additionalDirectories"]
    .as_array_mut()
    .ok_or("additionalDirectories ist kein Array")?;
  let before = dirs.len();
  dirs.retain(|d| d.as_str() != Some(dir));
  if dirs.len() == before {
    return Err(format!("nicht eingetragen: {dir}"));
  }
  if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
    allow.retain(|p| p.as_str() != Some(&format!("Edit({dir}/**)")));
  }
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}

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


/// Trägt einen Arbeitsordner in die Projekt-settings.json ein — wie im
/// Wizard: permissions.additionalDirectories + Edit-Permission. Legt die
/// Datei an, wenn sie fehlt (importierte Projekte).
fn add_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
  check_name(name)?;
  let wd_path = expand_home(paths, dir);
  if !wd_path.is_dir() {
    return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));
  }
  let dir = contract_home(paths, &wd_path);
  let sp = settings_path(&project_dir(paths, name)?);
  let mut v: serde_json::Value = if sp.is_file() {
    let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?
  } else {
    serde_json::json!({})
  };
  let root = v.as_object_mut().ok_or("settings.json ist kein Objekt")?;
  let perms = root
    .entry("permissions")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
    .ok_or("permissions ist kein Objekt")?;
  let dirs = perms
    .entry("additionalDirectories")
    .or_insert_with(|| serde_json::json!([]))
    .as_array_mut()
    .ok_or("additionalDirectories ist kein Array")?;
  if dirs.iter().any(|d| d.as_str() == Some(&dir)) {
    return Err(format!("schon eingetragen: {dir}"));
  }
  dirs.push(serde_json::json!(dir));
  let allow = perms
    .entry("allow")
    .or_insert_with(|| serde_json::json!([]))
    .as_array_mut()
    .ok_or("allow ist kein Array")?;
  allow.insert(0, serde_json::json!(format!("Edit({dir}/**)")));
  let parent = sp.parent().ok_or("settings.json ohne Elternordner")?;
  fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}

/// Nimmt einen Arbeitsordner wieder raus: additionalDirectories-Eintrag und
/// zugehörige Edit-Permission. Der Ordner selbst bleibt.
fn remove_work_dir_in(paths: &Paths, name: &str, dir: &str) -> Result<(), String> {
  check_name(name)?;
  let sp = settings_path(&project_dir(paths, name)?);
  let raw = fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?;
  let mut v: serde_json::Value =
    serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", sp.display()))?;
  let perms = v["permissions"]
    .as_object_mut()
    .ok_or("permissions ist kein Objekt")?;
  let dirs = perms["additionalDirectories"]
    .as_array_mut()
    .ok_or("additionalDirectories ist kein Array")?;
  let before = dirs.len();
  dirs.retain(|d| d.as_str() != Some(dir));
  if dirs.len() == before {
    return Err(format!("nicht eingetragen: {dir}"));
  }
  if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
    allow.retain(|p| p.as_str() != Some(&format!("Edit({dir}/**)")));
  }
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
}

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

+/// Trägt einen Arbeitsordner in die Projekt-settings.json ein — wie im

+/// Wizard: permissions.additionalDirectories + Edit-Permission. Legt die

+/// Datei an, wenn sie fehlt (importierte Projekte).

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

+  let wd_path = expand_home(paths, dir);

+    return Err(format!("Arbeitsverzeichnis fehlt: {}", wd_path.display()));

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

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

+  let mut v: serde_json::Value = if sp.is_file() {

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

+  let root = v.as_object_mut().ok_or("settings.json ist kein Objekt")?;

+    .or_insert_with(|| serde_json::json!({}))

+    .ok_or("permissions ist kein Objekt")?;

+    .or_insert_with(|| serde_json::json!([]))

+    .ok_or("additionalDirectories ist kein Array")?;

+  if dirs.iter().any(|d| d.as_str() == Some(&dir)) {

+    return Err(format!("schon eingetragen: {dir}"));

+  allow.insert(0, serde_json::json!(format!("Edit({dir}/**)")));

+  let parent = sp.parent().ok_or("settings.json ohne Elternordner")?;

+  fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;

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

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

+/// Nimmt einen Arbeitsordner wieder raus: additionalDirectories-Eintrag und

+/// zugehörige Edit-Permission. Der Ordner selbst bleibt.

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

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

+  let dirs = perms["additionalDirectories"]

+  dirs.retain(|d| d.as_str() != Some(dir));

+    return Err(format!("nicht eingetragen: {dir}"));

+  if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {

+    allow.retain(|p| p.as_str() != Some(&format!("Edit({dir}/**)")));

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


nicht addtional  wir hatten die - zumindest auch - unter permissions


#[tauri::command]
fn todo_state(project: String) -> Result<bool, String> {
  todo_state_in(&Paths::real(), &project)
}

#[tauri::command]
fn add_work_dir(project: String, dir: String) -> Result<(), String> {
  add_work_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn remove_work_dir(project: String, dir: String) -> Result<(), String> {
  remove_work_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn todo_state(project: String) -> Result<bool, String> {
  todo_state_in(&Paths::real(), &project)
}


#[tauri::command]
fn add_work_dir(project: String, dir: String) -> Result<(), String> {
  add_work_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn remove_work_dir(project: String, dir: String) -> Result<(), String> {
  remove_work_dir_in(&Paths::real(), &project, &dir)
}

#[tauri::command]
fn todo_state(project: String) -> Result<bool, String> {
  todo_state_in(&Paths::real(), &project)
}

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

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

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

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


      delete_project,
      project_work_dirs,

      delete_project,
      project_work_dirs,
      add_work_dir,
      remove_work_dir,


      delete_project,
      project_work_dirs,

      delete_project,
      project_work_dirs,
      add_work_dir,
      remove_work_dir,


Bash

grep -n "fn projekt_arbeitsordner_auslesen" -A 16 /home/marcuh/projects/ai-control/src-tauri/src/lib.rs

Read work-dir test for anchor


2724:  fn projekt_arbeitsordner_auslesen() {
2725-    let p = tmp_paths();
2726-    create_project_full_in(&p, "proj", None, None, Some("~/projects/proj"), true, TerminalConfig::default(), false)
2727-      .unwrap();
2728-    assert_eq!(project_work_dirs_in(&p, "proj").unwrap(), vec!["~/projects/proj"]);
2729-    // Projekt ohne settings.json → leer
2730-    create_project_in(&p, "alt").unwrap();
2731-    assert!(project_work_dirs_in(&p, "alt").unwrap().is_empty());
2732-  }
2733-
2734-  #[test]
2735-  fn projekt_loeschen_unbekannt_scheitert() {
2736-    let p = tmp_paths();
2737-    assert!(delete_project_in(&p, "gibtsnicht", false).is_err());
2738-  }
2739-}


Edit

  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {
    let p = tmp_paths();
    assert!(delete_project_in(&p, "gibtsnicht", false).is_err());
  }
}

  #[test]
  fn arbeitsordner_nachtraeglich_erfassen_und_entfernen() {
    let p = tmp_paths();
    create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)
      .unwrap();
    fs::create_dir_all(p.home.join("projects/extra")).unwrap();
    let picked = p.home.join("projects/extra").to_string_lossy().into_owned();
    add_work_dir_in(&p, "proj", &picked).unwrap();
    // gespeichert wird ~-kontrahiert, in beiden permissions-Feldern
    assert_eq!(project_work_dirs_in(&p, "proj").unwrap(), vec!["~/projects/extra"]);
    let raw = fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap();
    assert!(raw.contains("Edit(~/projects/extra/**)"));
    assert!(add_work_dir_in(&p, "proj", &picked).is_err()); // doppelt

    remove_work_dir_in(&p, "proj", "~/projects/extra").unwrap();
    assert!(project_work_dirs_in(&p, "proj").unwrap().is_empty());
    let raw = fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap();
    assert!(!raw.contains("~/projects/extra"));
    // Ordner selbst bleibt
    assert!(p.home.join("projects/extra").is_dir());
  }

  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {
    let p = tmp_paths();
    assert!(delete_project_in(&p, "gibtsnicht", false).is_err());
  }
}


  #[test]
  fn arbeitsordner_nachtraeglich_erfassen_und_entfernen() {
    let p = tmp_paths();
    create_project_full_in(&p, "proj", None, None, None, false, TerminalConfig::default(), false)
      .unwrap();
    fs::create_dir_all(p.home.join("projects/extra")).unwrap();
    let picked = p.home.join("projects/extra").to_string_lossy().into_owned();
    add_work_dir_in(&p, "proj", &picked).unwrap();
    // gespeichert wird ~-kontrahiert, in beiden permissions-Feldern
    assert_eq!(project_work_dirs_in(&p, "proj").unwrap(), vec!["~/projects/extra"]);
    let raw = fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap();
    assert!(raw.contains("Edit(~/projects/extra/**)"));
    assert!(add_work_dir_in(&p, "proj", &picked).is_err()); // doppelt

    remove_work_dir_in(&p, "proj", "~/projects/extra").unwrap();
    assert!(project_work_dirs_in(&p, "proj").unwrap().is_empty());
    let raw = fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap();
    assert!(!raw.contains("~/projects/extra"));
    // Ordner selbst bleibt
    assert!(p.home.join("projects/extra").is_dir());
  }

  #[test]
  fn projekt_loeschen_unbekannt_scheitert() {
    let p = tmp_paths();
    assert!(delete_project_in(&p, "gibtsnicht", false).is_err());
  }
}

+  fn arbeitsordner_nachtraeglich_erfassen_und_entfernen() {

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

+    fs::create_dir_all(p.home.join("projects/extra")).unwrap();

+    let picked = p.home.join("projects/extra").to_string_lossy().into_owned();

+    add_work_dir_in(&p, "proj", &picked).unwrap();

+    // gespeichert wird ~-kontrahiert, in beiden permissions-Feldern

+    assert_eq!(project_work_dirs_in(&p, "proj").unwrap(), vec!["~/projects/extra"]);

+    let raw = fs::read_to_string(settings_path(&p.projects_dir().join("proj"))).unwrap();

+    assert!(raw.contains("Edit(~/projects/extra/**)"));

+    assert!(add_work_dir_in(&p, "proj", &picked).is_err()); // doppelt

+    remove_work_dir_in(&p, "proj", "~/projects/extra").unwrap();

+    assert!(project_work_dirs_in(&p, "proj").unwrap().is_empty());

+    assert!(!raw.contains("~/projects/extra"));

+    assert!(p.home.join("projects/extra").is_dir());


Bash

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

Run backend tests


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

     Running unittests src/main.rs (src-tauri/target/debug/deps/ai_control-cb387f5a798b9c01)

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

   Doc-tests app_lib

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


Backend grün (49 Tests). Jetzt UI — Verwaltung im ⚙-Dialog plus Umbenennung des Import-Buttons:


Edit

    addFolder: "+ Ordner",
    addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",

    addFolder: "Projekt importieren …",
    addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
    addWorkDir: "Ordner hinzufügen …",


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

    addFolder: "Projekt importieren …",
    addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
    addWorkDir: "Ordner hinzufügen …",

import { createI18n } from "vue-i18n";

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

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

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

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

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

+    addFolder: "Projekt importieren …",


    addFolder: "+ Folder",
    addFolderTitle: "Add an existing folder as a project",

    addFolder: "Import project …",
    addFolderTitle: "Add an existing folder as a project",
    addWorkDir: "Add folder …",


    addFolder: "Import project …",
    addFolderTitle: "Add an existing folder as a project",
    addWorkDir: "Add folder …",

import { createI18n } from "vue-i18n";

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

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

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

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

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


interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
}

const settings = ref<TerminalSettings | null>(null);

async function openSettings(p: Project) {
  try {
    const todo = await invoke<boolean>("todo_state", { project: p.name });
    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",
      icon: p.terminal.icon,
      title: p.terminal.title ?? "",
      todo,
    };
  } catch (e) {
    error.value = String(e);
  }
}

interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}

const settings = ref<TerminalSettings | null>(null);

async function openSettings(p: Project) {
  try {
    const todo = await invoke<boolean>("todo_state", { project: p.name });
    const workDirs = await invoke<string[]>("project_work_dirs", {
      project: p.name,
    });
    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",
      icon: p.terminal.icon,
      title: p.terminal.title ?? "",
      todo,
      workDirs,
    };
  } catch (e) {
    error.value = String(e);
  }
}

// Arbeitsordner schreiben direkt in die Projekt-settings.json
// (additionalDirectories + Edit-Permission) — kein Speichern-Schritt.
async function addWorkDir() {
  const s = settings.value!;
  const dir = await open({ directory: true, multiple: false });
  if (typeof dir !== "string") return;
  try {
    await invoke("add_work_dir", { project: s.name, dir });
    s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
  } catch (e) {
    error.value = String(e);
  }
}

async function removeWorkDir(dir: string) {
  const s = settings.value!;
  try {
    await invoke("remove_work_dir", { project: s.name, dir });
    s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
  } catch (e) {
    error.value = String(e);
  }
}


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

interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
}

const settings = ref<TerminalSettings | null>(null);

async function openSettings(p: Project) {
  try {
    const todo = await invoke<boolean>("todo_state", { project: p.name });
    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",
      icon: p.terminal.icon,
      title: p.terminal.title ?? "",
      todo,
    };
  } catch (e) {
    error.value = String(e);
  }
}

interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}

const settings = ref<TerminalSettings | null>(null);

async function openSettings(p: Project) {
  try {
    const todo = await invoke<boolean>("todo_state", { project: p.name });
    const workDirs = await invoke<string[]>("project_work_dirs", {
      project: p.name,
    });
    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",
      icon: p.terminal.icon,
      title: p.terminal.title ?? "",
      todo,
      workDirs,
    };
  } catch (e) {
    error.value = String(e);
  }
}

// Arbeitsordner schreiben direkt in die Projekt-settings.json
// (additionalDirectories + Edit-Permission) — kein Speichern-Schritt.
async function addWorkDir() {
  const s = settings.value!;
  const dir = await open({ directory: true, multiple: false });
  if (typeof dir !== "string") return;
  try {
    await invoke("add_work_dir", { project: s.name, dir });
    s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
  } catch (e) {
    error.value = String(e);
  }
}

async function removeWorkDir(dir: string) {
  const s = settings.value!;
  try {
    await invoke("remove_work_dir", { project: s.name, dir });
    s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });
  } catch (e) {
    error.value = String(e);
  }
}

+    const workDirs = await invoke<string[]>("project_work_dirs", {

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

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

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

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

+    s.workDirs = await invoke<string[]>("project_work_dirs", { project: s.name });

+async function removeWorkDir(dir: string) {

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


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

          <input v-model="settings.todo" type="checkbox" />
          {{ $t("projects.todoDesc") }}

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

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

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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

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

          <input v-model="settings.todo" type="checkbox" />
          {{ $t("projects.todoDesc") }}

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


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

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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

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

          <input v-model="settings.todo" type="checkbox" />
          {{ $t("projects.todoDesc") }}

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

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

+            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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


Bash

grep -n "\.icon-row\b" /home/marcuh/projects/ai-control/src/style.css | head -2

Check icon-row style exists


580:.icon-row {


grep -n "\.icon-row" -A 5 /home/marcuh/projects/ai-control/src/style.css | head -8

Read icon-row style


580:.icon-row {
581-  display: flex;
582-  align-items: center;
583-  gap: 0.5rem;
584-}
585-


Edit

.icon-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.icon-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}


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

.icon-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

gap: 0.5rem;
}
+.workdirs {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 0.35rem;
+}
+
.icon-path {
font-family: var(--mono);
color: var(--overlay);


Bash

npm run build 2>&1 | grep -E "error|✓ built" | head -5

Build frontend


✓ built in 232ms


Fertig, Frontend und Backend bauen; 49/49 Tests grün.

- **⚙-Dialog** hat jetzt einen Arbeitsordner-Abschnitt: bestehende Einträge mit „Entfernen", darunter „Ordner hinzufügen …" mit Picker. Änderungen schreiben sofort in die Projekt-`settings.json` — Ordner landet ~-kontrahiert in `permissions.additionalDirectories` **und** als `Edit(<ordner>/**)` in `permissions.allow`; Entfernen räumt beide Einträge, der Ordner selbst bleibt. Bei importierten Projekten ohne `settings.json` wird die Datei angelegt.
- **Button umbenannt:** „+ Folder" heißt jetzt „Projekt importieren …" / „Import project …".
- Doppelte Erfassung wird abgewiesen; neuer Test deckt Erfassen/Entfernen ab.

Build/Deploy und der inzwischen große Commit-Stapel (Icons zentral, Crash-Fix, README/LICENSE, CSS, claudeCommand, Arbeitsordner-Verwaltung) auf dein Go.


go


git add -A && git commit -m 'Icons zentral unter ~/.config/ai-control/icons (pool-/repo-unabhängig), nicht ladbares Icon crasht den Tray nicht mehr; Arbeitsordner nachträglich im ⚙-Dialog erfassbar (additionalDirectories + Edit-Permission); Import-Button umbenannt; claudeCommand konfigurierbar + CLAUDE_CONFIG_DIR aus Pool; Pool-Tabellen-Layout; README englisch mit Screenshots; MIT-Lizenz' && git push 2>&1 | tail -2 && ./build.sh 2>&1 | tail -1 && rm -rf ~/Applications/ai-control.app && cp -R src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ && /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/ai-control.app && echo DEPLOYED

Commit, push, build, deploy and re-register app


[main 0908385] Icons zentral unter ~/.config/ai-control/icons (pool-/repo-unabhängig), nicht ladbares Icon crasht den Tray nicht mehr; Arbeitsordner nachträglich im ⚙-Dialog erfassbar (additionalDirectories + Edit-Permission); Import-Button umbenannt; claudeCommand konfigurierbar + CLAUDE_CONFIG_DIR aus Pool; Pool-Tabellen-Layout; README englisch mit Screenshots; MIT-Lizenz
 14 files changed, 330 insertions(+), 84 deletions(-)
 create mode 100644 LICENSE
 create mode 100644 docs/pools.png
 create mode 100644 docs/projects.png
 create mode 100644 docs/terminal.png
 create mode 100644 docs/tray.png
 create mode 100644 docs/usage.png
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   b5995da..0908385  main -> main
 from spotlightDEPLOYED


Erledigt: Commit `0908385` gepusht (14 Dateien, inkl. LICENSE und `docs/`-Screenshots), Release gebaut, nach `~/Applications` deployed und diesmal direkt per `lsregister` registriert — das „App weg"-Problem nach Deploys sollte damit nicht wieder auftreten.

Nach dem App-Neustart ist alles drauf: zentrale Icons, Tray-Robustheit, Arbeitsordner-Verwaltung im ⚙-Dialog, umbenannter Import-Button. Der Stand im Repo ist damit GitHub-reif — READMEs, Lizenz und Screenshots inklusive.


hmmm - die Project config dialog ist verutscht
das label WorkingFolder hängt zu tief
und warum dśind bei den zusätzlichen Ordnern nicht die Ordner meiner Projekt angezeigt?


hmmm - die Project config dialog ist verutscht das label WorkingFolder hängt zu tief und warum dśind bei den zusätzlichen Ordnern nicht die Ordner meiner Projekt angezeigt?


for p in 08-07 ai-control limbach misc robotunits wittgenstein wuerth-plato; do echo "== $p"; cat ~/claude-projects/$p/.claude/settings.json 2>/dev/null | head -25; done

Inspect project settings.json files


== 08-07
{
  "autoMemoryDirectory": "~/claude-projects/08-07/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/08-07/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/08-07"
    ],
    "allow": [
      "Edit(~/projects/08-07/**)",
      "Edit(~/claude-projects/08-07/**)"
    ]
  }
}
== ai-control
{
  "autoMemoryDirectory": "~/claude-projects/ai-control/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/ai-control/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/ai-control"
    ],
    "allow": [
      "Edit(~/projects/ai-control/**)",
      "Edit(~/claude-projects/ai-control/**)"
    ]
  }
}
== limbach
{
  "autoMemoryDirectory": "~/claude-projects/limbach/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/limbach/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/limbach"
    ],
    "allow": [
      "Edit(~/projects/limbach/**)",
      "Edit(~/claude-projects/limbach/**)"
    ]
  }
}
== misc
{
  "autoMemoryDirectory": "~/claude-projects/misc/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/misc/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/misc"
    ],
    "allow": [
      "Edit(~/projects/misc/**)"
    ]
  }
}
== robotunits
{
  "autoMemoryDirectory": "~/claude-projects/robotunits/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/robotunits/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/geko-core",
      "~/projects/adesso-cpq",
      "~/projects/geko-infra",
      "~/projects/geko-fire"
    ],
    "allow": [
      "Edit(~/projects/geko-core/**)",
      "Edit(~/projects/adesso-cpq/**)",
      "Edit(~/projects/geko-infra/**)",
== wittgenstein
{
  "autoMemoryDirectory": "~/claude-projects/wittgenstein/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /home/marcuh/claude-projects/wittgenstein/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/wittgenstein"
    ],
    "allow": [
      "Edit(~/projects/wittgenstein/**)",
      "Edit(~/claude-projects/wittgenstein/**)"
    ]
  }
}
== wuerth-plato
{
  "autoMemoryDirectory": "~/claude-projects/wuerth-plato/memory",
  "hooks": {
    "SessionStart": []
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/fahrzeugeinrichtung-plato-backend",
      "~/projects/fahrzeugeinrichtung-plato-gitops"
    ],
    "allow": [
      "Edit(~/projects/fahrzeugeinrichtung-plato-backend/**)",
      "Edit(~/projects/fahrzeugeinrichtung-plato-gitops/**)",
      "Edit(~/claude-projects/wuerth-plato/**)"
    ]
  }
}


grep -n "fn project_work_dirs" -B 2 -A 4 src-tauri/src/lib.rs; grep -n "\.field\b" -A 8 src/style.css | head -14

Check work-dirs command and field CSS


657-
658-/// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json.
659:fn project_work_dirs_in(paths: &Paths, name: &str) -> Result<Vec<String>, String> {
660-  check_name(name)?;
661-  let sp = settings_path(&project_dir(paths, name)?);
662-  if !sp.is_file() {
663-    return Ok(Vec::new());
--
1574-
1575-#[tauri::command]
1576:fn project_work_dirs(project: String) -> Result<Vec<String>, String> {
1577-  project_work_dirs_in(&Paths::real(), &project)
1578-}
1579-
1580-#[tauri::command]
571:.dialog .field {
572-  display: grid;
573-  grid-template-columns: 6rem 1fr;
574-  align-items: center;
575-  gap: 0.75rem;
576-  color: var(--subtext);
577-  font-size: 0.85rem;
578-}
579-
--
636:.dialog .field .checkline {
637-  font-size: 0.85rem;
638-  color: var(--subtext);
639-}


grep -n "workdirs\|addWorkDir\|removeWorkDir" src/components/ProjectList.vue | head; grep -n "icon-path" -A 4 src/style.css | head -12

Verify template placement and icon-path style


185:async function addWorkDir() {
197:async function removeWorkDir(dir: string) {
622:        <span class="workdirs">
625:            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>
627:          <button @click="addWorkDir">{{ $t("projects.addWorkDir") }}</button>
593:.icon-path {
594-  font-family: var(--mono);
595-  color: var(--overlay);
596-  font-size: 0.75rem;
597-  max-width: 11rem;


560

561

562
{{ $t("projects.deleteTitle", { name: pendingDelete.name }) }}

563
{{ $t("projects.deleteWarning", { name: pendingDelete.name }) }}

564	      <template v-if="pendingDelete.workDirs.length">
565
566	          <input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
567	          {{ $t("projects.deleteWorkDirs") }}
568
569

570
{{ wd }}

571

572

573	          {{ $t("projects.workDirsStay") }}
574

575	      </template>
576
{{ $t("projects.unlinkHint") }}

577

578	        <button type="button" @click="pendingDelete = null">
579	          {{ $t("projects.cancel") }}
580	        </button>
581	        <button @click="unlinkProject">
582	          {{ $t("projects.unlink") }}
583	        </button>
584	        <button class="danger" @click="confirmDelete">
585	          {{ $t("projects.delete") }}
586	        </button>
587

588

589

590
591

592

593
{{ $t("projects.terminal", { name: settings.name }) }}

594
595	        {{ $t("projects.title") }}
596	        <input v-model="settings.title" :placeholder="settings.name" />
597
598
599	        {{ $t("projects.theme") }}
600	        <select v-model="settings.theme">
601	          <option v-for="[value, label] in THEME_NAMES" :key="value" :value="value">
602	            {{ label }}
603	          </option>
604	        </select>
605
606
607	        {{ $t("projects.dockIcon") }}
608
609	          <button @click="pickIcon">{{ $t("projects.chooseFile") }}</button>
610	          <button v-if="settings.icon" @click="settings.icon = null">
611	            {{ $t("projects.remove") }}
612	          </button>
613
614	            {{ settings.icon.split("/").pop() }}
615	            {{ settings.icon }}
616
617	          {{ $t("projects.defaultIcon") }}
618
619
620
621	        {{ $t("projects.workDir") }}
622
623
624	            {{ wd }}
625	            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>
626
627	          <button @click="addWorkDir">{{ $t("projects.addWorkDir") }}</button>
628
629
630
631	        {{ $t("projects.todo") }}
632
633	          <input v-model="settings.todo" type="checkbox" />
634	          {{ $t("projects.todoDesc") }}
635
636
637
{{ $t("projects.appliesNextStart") }}

638

639	        <button @click="settings = null">{{ $t("projects.cancel") }}</button>
640	        <button class="primary" @click="saveSettings">
641	          {{ $t("projects.save") }}
642	        </button>
643

644

{{ $t("projects.deleteTitle", { name: pendingDelete.name }) }}

{{ $t("projects.deleteWarning", { name: pendingDelete.name }) }}

      <template v-if="pendingDelete.workDirs.length">

          <input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
          {{ $t("projects.deleteWorkDirs") }}

{{ wd }}

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

      </template>

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

        <button type="button" @click="pendingDelete = null">
          {{ $t("projects.cancel") }}
        </button>
        <button @click="unlinkProject">
          {{ $t("projects.unlink") }}
        </button>
        <button class="danger" @click="confirmDelete">
          {{ $t("projects.delete") }}
        </button>

{{ $t("projects.terminal", { name: settings.name }) }}

        {{ $t("projects.title") }}
        <input v-model="settings.title" :placeholder="settings.name" />

        {{ $t("projects.theme") }}
        <select v-model="settings.theme">
          <option v-for="[value, label] in THEME_NAMES" :key="value" :value="value">
            {{ label }}
          </option>
        </select>

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

          <button @click="pickIcon">{{ $t("projects.chooseFile") }}</button>
          <button v-if="settings.icon" @click="settings.icon = null">
            {{ $t("projects.remove") }}
          </button>

            {{ settings.icon.split("/").pop() }}
            {{ settings.icon }}

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

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

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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

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

          <input v-model="settings.todo" type="checkbox" />
          {{ $t("projects.todoDesc") }}

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

        <button @click="settings = null">{{ $t("projects.cancel") }}</button>
        <button class="primary" @click="saveSettings">
          {{ $t("projects.save") }}
        </button>


Edit

.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

/* Mehrzeilige Felder: Label oben ausrichten statt vertikal zentriert. */
.dialog .field.field-top {
  align-items: start;
  padding-top: 0.15rem;
}


.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

/* Mehrzeilige Felder: Label oben ausrichten statt vertikal zentriert. */
.dialog .field.field-top {
  align-items: start;
  padding-top: 0.15rem;
}

+/* Mehrzeilige Felder: Label oben ausrichten statt vertikal zentriert. */


      <label class="field">
        {{ $t("projects.workDir") }}
        <span class="workdirs">

      <label class="field field-top">
        {{ $t("projects.workDir") }}
        <span class="workdirs">


      <label class="field">
        {{ $t("projects.workDir") }}
        <span class="workdirs">

      <label class="field field-top">
        {{ $t("projects.workDir") }}
        <span class="workdirs">

<span v-else class="icon-path">{{ $t("projects.defaultIcon") }}</span>

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


Bash

npm run build 2>&1 | grep -E "error|✓ built" | head -3 && ./build.sh 2>&1 | tail -1 && rm -rf ~/Applications/ai-control.app && cp -R src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ && /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/ai-control.app && echo DEPLOYED

Build and deploy label fix


✓ built in 245ms
 from spotlightDEPLOYED

✓ built in 245ms
 from spotlightDEPLOYED


Label-Fix gebaut und deployed. Also: aktuelle Instanz beenden, `ai-control` neu starten, dann ⚙ auf einem deiner Projekte — erwartet: Arbeitsordner-Liste gefüllt (z. B. robotunits mit vier Einträgen), Label „Working folder" oben bündig. Falls die Liste dann immer noch leer ist, ist es ein echter Bug und ich grabe weiter. Der Fix ist noch nicht committet — kommt mit dem nächsten Stapel.


Immer noch falsch
ai-control z.B.
Working foldrer: claude-projects/ai-control
zusätzlich projects/ai-control


Verstanden — die Anzeige ist irreführend: Unter „Working folder" erscheint nur `~/projects/ai-control` (der Zusatzordner), als wäre er das Arbeitsverzeichnis. Richtig: erst der Projektordner selbst (`claude-projects/ai-control`, fest), darunter die zusätzlichen Ordner. Baue ich so:


Edit

interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}

interface TerminalSettings {
  name: string;
  path: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}


interface TerminalSettings {
  name: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}

interface TerminalSettings {
  name: string;
  path: string;
  theme: string;
  icon: string | null;
  title: string;
  todo: boolean;
  workDirs: string[];
}

interface TerminalSettings {
name: string;
+  path: string;
theme: string;
icon: string | null;
title: string;


    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",

    settings.value = {
      name: p.name,
      path: p.path,
      theme: p.terminal.theme ?? "mocha",


    settings.value = {
      name: p.name,
      theme: p.terminal.theme ?? "mocha",

    settings.value = {
      name: p.name,
      path: p.path,
      theme: p.terminal.theme ?? "mocha",


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

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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

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

            {{ settings.path }}

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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


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

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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

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

            {{ settings.path }}

            {{ wd }}
            <button @click="removeWorkDir(wd)">{{ $t("projects.remove") }}</button>

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


.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

.workdirs .icon-path {
  max-width: none;
}


.workdirs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

.workdirs .icon-path {
  max-width: none;
}

/* Mehrzeilige Felder: Label oben ausrichten statt vertikal zentriert. */


Bash

npm run build 2>&1 | grep -E "error|✓ built" | head -3 && ./build.sh 2>&1 | tail -1 && rm -rf ~/Applications/ai-control.app && cp -R src-tauri/target/release/bundle/macos.noindex/ai-control.app ~/Applications/ && /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/Applications/ai-control.app && echo DEPLOYED

Build and deploy work-folder display fix


✓ built in 252ms
 from spotlightDEPLOYED

✓ built in 252ms
 from spotlightDEPLOYED


Deployed — nach App-Neustart zeigt der ⚙-Dialog unter „Working folder" jetzt zuerst den Projektordner selbst (fest, ohne Entfernen-Button, z. B. `/Users/…/claude-projects/ai-control`), darunter die zusätzlichen Ordner (`~/projects/ai-control`) mit „Entfernen", zuletzt „Ordner hinzufügen …". Lange Pfade werden nicht mehr auf 11rem gestutzt.


ok -- warum der Working Folder ohne Tilde?


Weil der Projektpfad aus `list_projects` kommt und dort absolut serialisiert wird (`dir.to_string_lossy()`), während die Zusatzordner ~-kontrahiert in der settings.json stehen. Ich kontrahiere den Projektpfad ebenfalls — gilt dann auch für die Pfadzeile in der Projektliste:


Edit

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

  for (name, dir) in load_registry(paths)? {
    let cfg = read_project_config_in(paths, &name)?;
    projects.push(Project {
      running: is_running(&name),
      path: contract_home(paths, &dir),
      pool: cfg.pool,
      terminal: cfg.terminal,
      name,
    });
  }


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

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

  for (name, dir) in load_registry(paths)? {
    let cfg = read_project_config_in(paths, &name)?;
    projects.push(Project {
      running: is_running(&name),
      path: contract_home(paths, &dir),
      pool: cfg.pool,
      terminal: cfg.terminal,
      name,
    });
  }

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

+      path: contract_home(paths, &dir),


Bash

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

Test, build and deploy tilde path fix


test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.84s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
 from spotlightDEPLOYED


Deployed (Tests grün). Nach Neustart: Projektpfade überall ~-kontrahiert — im ⚙-Dialog (`~/claude-projects/ai-control`) wie in der Projektliste; Pfade außerhalb von Home blieben absolut. Die beiden UI-Fixes (Working-folder-Anzeige, Tilde) sind noch nicht committet — kommen mit dem nächsten Stapel auf dein Go.


go


git add -A && git commit -m 'Working-folder-Anzeige im ⚙-Dialog: Projektordner zuerst (fest), dann Zusatzordner mit Entfernen; Label oben ausgerichtet; Projektpfade überall ~-kontrahiert' && git push 2>&1 | tail -2

Commit and push UI fixes


[main 2075f9a] Working-folder-Anzeige im ⚙-Dialog: Projektordner zuerst (fest), dann Zusatzordner mit Entfernen; Label oben ausgerichtet; Projektpfade überall ~-kontrahiert
 3 files changed, 17 insertions(+), 2 deletions(-)
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   0908385..2075f9a  main -> main


Commit `2075f9a` gepusht. Repo und deployte App sind damit auf demselben Stand — GitHub-reif: englisches README mit Screenshots, MIT-Lizenz, konfigurierbares `claudeCommand`, freie Projekt-Registry, zentrale Icons, Arbeitsordner-Verwaltung.


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


Kein technischer Grund — ich hatte den Eintrag als reine Anzeige gebaut, weil er die Registry-Zuordnung ist. Änderbar ist der problemlos: Registry-Eintrag auf den neuen Ordner zeigen lassen; die Verweise auf den alten Root in der settings.json (`autoMemoryDirectory`, `Edit(<root>/**)`) ziehen mit um. Semantik: du wählst den Ordner, in dem das Projekt jetzt liegt — verschieben tut die App nichts, und bei laufender Session ist es gesperrt. Baue ich:


Edit

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

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

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

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