    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.lang = "de";
    editor.hidden = true;

import { marked } from "marked";

export interface PanelView {
  set(text: string): void;
  raw(): string;
}

/// Verkabelt einen Panel-Inhaltsbereich: MD/Roh-Umschalter und Copy-Button.
/// Der Rohtext bleibt die Quelle für „Kopieren"; die Ansicht rendert Markdown.
/// Entfernt Inline-Markdown (Emphasis, Code, Links), damit der Plain-Text-Titel
/// nicht die rohen Marker zeigt.
function stripInlineMd(s: string): string {
  return s
    .replace(/`([^`]*)`/g, "$1")
    .replace(/\*\*([^*]+)\*\*/g, "$1")
    .replace(/\*([^*]+)\*/g, "$1")
    .replace(/__([^_]+)__/g, "$1")
    .replace(/_([^_]+)_/g, "$1")
    .replace(/~~([^~]+)~~/g, "$1")
    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
    .trim();
}

/// Titel für den Panel-Kopf: erste Überschrift (# …) oder sonst erste
/// nicht-leere Zeile, ohne Inline-Markdown; „Entwurf" bei leerem Text.
function firstLine(text: string): string {
  let fallback = "";
  for (const line of text.split("\n")) {
    const t = line.trim();
    if (!t) continue;
    const h = t.replace(/^#+/, "").trim();
    if (t.startsWith("#") && h) return stripInlineMd(h);
    if (!fallback) fallback = t;
  }
  return fallback ? stripInlineMd(fallback) : "Entwurf";
}

/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
function setHeading(text: string, title: string): string {
  const lines = text.split("\n");
  for (let i = 0; i < lines.length; i++) {
    const t = lines[i].trim();
    if (!t) continue;
    if (t.startsWith("#")) {
      const hashes = (t.match(/^#+/) as RegExpMatchArray)[0];
      lines[i] = `${hashes} ${title}`;
    } else {
      lines.splice(i, 0, `# ${title}`, "");
    }
    return lines.join("\n");
  }
  return `# ${title}\n`;
}

export function initPanelView(opts: {
  content: HTMLElement;
  copyBtn: HTMLElement;
  modeBtn: HTMLElement;
  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;
  // Content-Edit-Zustand: während des Editierens werden eingehende Updates
  // gepuffert statt angewandt.
  let editing = false;
  let pending: string | null = null;

  function draw() {
    if (rendered) {
      opts.content.className = "md";
      opts.content.innerHTML = marked.parse(rawText, { async: false });
    } else {
      opts.content.className = "raw";
      opts.content.textContent = rawText;
    }
    opts.modeBtn.textContent = rendered ? "MD" : "Roh";
  }

  opts.modeBtn.addEventListener("click", () => {
    rendered = !rendered;
    draw();
  });

  opts.copyBtn.addEventListener("click", async () => {
    await navigator.clipboard.writeText(rawText);
    opts.copyBtn.classList.add("copied");
    setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
  });

  // Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
  // geänderte Überschrift zurück, Escape verwirft.
  const titleEl = opts.titleEl;
  if (opts.editBtn && titleEl && opts.onCommit) {
    const commit = () => {
      if (titleEl.getAttribute("contenteditable") !== "true") return;
      titleEl.removeAttribute("contenteditable");
      const nt = (titleEl.textContent || "").trim();
      if (nt && nt !== firstLine(rawText)) opts.onCommit!(setHeading(rawText, nt));
      else titleEl.textContent = firstLine(rawText);
    };
    opts.editBtn.addEventListener("click", () => {
      titleEl.setAttribute("contenteditable", "true");
      titleEl.focus();
      const r = document.createRange();
      r.selectNodeContents(titleEl);
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(r);
    });
    titleEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        titleEl.blur();
      } else if (e.key === "Escape") {
        titleEl.textContent = firstLine(rawText);
        titleEl.removeAttribute("contenteditable");
        titleEl.blur();
      }
    });
    titleEl.addEventListener("blur", commit);
  }

  // Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
  // (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
  // Eingehende Updates während des Editierens werden gepuffert.
  if (opts.editContentBtn && opts.onCommit) {
    const editBtn = opts.editContentBtn;
    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = false;
    editor.hidden = true;
    opts.content.after(editor);

    const leave = () => {
      editing = false;
      editor.hidden = true;
      opts.content.hidden = false;
      editBtn.classList.remove("active", "changed");
    };
    const enter = () => {
      editing = true;
      pending = null;
      editor.value = rawText;
      opts.content.hidden = true;
      editor.hidden = false;
      editBtn.classList.add("active");
      editor.focus();
    };
    const save = () => {
      const v = editor.value;
      leave();
      opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
    };
    const cancel = () => {
      const p = pending;
      leave();
      if (p !== null) applyText(p);
      else draw();
    };
    editBtn.addEventListener("click", () => (editing ? save() : enter()));
    editor.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        save();
      } else if (e.key === "Escape") {
        e.preventDefault();
        cancel();
      }
    });
  }

  function applyText(text: string) {
    rawText = text;
    if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
      titleEl.textContent = firstLine(text);
    }
    draw();
  }

  draw();
  return {
    set(text: string) {
      // Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den
      // Edit-Button als „geändert" markieren.
      if (editing) {
        pending = text;
        opts.editContentBtn?.classList.add("changed");
        return;
      }
      applyText(text);
    },
    raw: () => rawText,
  };
}


Bash

cd ~/projects/ai-control && npm run build 2>&1 | grep -E "error|Error|✓ built" | head

Frontend-Build


✓ built in 219ms


Gebaut. Der Content-Editor hat jetzt `spellcheck=true` und `lang="de"` — die Webview macht die Prüfung nativ (auf macOS WKWebView: rote Unterringelung, Korrekturen per Rechtsklick, ohne Zutun). Auf Linux hängt es an der WebKitGTK-Rechtschreibung (enchant + Wörterbücher) — das prüfen wir beim Linux-Umzug mit.

Reine Frontend-Änderung (`panel-view.ts`). Nichts committet.


Lang muss wählbar sein
User- Settings?? wir haben doch eine settings.json
und dann noch pro Text?


Ja, zwei Ebenen: Default in der App-`settings.json`, per-Text ein Selector im Panel, der den Default überschreibt. Ich schaue kurz, wie `terminal_font_size` die `settings.json` liest, um es gleich zu machen:


cd ~/projects/ai-control/src-tauri/src; grep -n "fn terminal_font_size\|fn set_terminal_font_size\|fn read_app_setting\|APP_SETTINGS_FILE\|fn app_settings" lib.rs | head

Settings-Read-Muster finden


391:const APP_SETTINGS_FILE: &str = "settings.json";
395:  fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
404:  fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
420:  let path = paths.config_dir().join(APP_SETTINGS_FILE);
1535:  fs::read_to_string(paths.config_dir().join(APP_SETTINGS_FILE))
2148:fn terminal_font_size() -> u32 {
2149:  fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
2159:fn set_terminal_font_size(size: u32) -> Result<(), String> {
2161:  let path = paths.config_dir().join(APP_SETTINGS_FILE);
3212:      p.config_dir().join(APP_SETTINGS_FILE),


Lang muss wählbar sein User- Settings?? wir haben doch eine settings.json und dann noch pro Text?


Add dockable text panel with copy functionality to terminal


2145	/// Terminal-Schriftgröße (settings.json: terminalFontSize), ein Wert für alle
2146	/// Terminals. Default 13.
2147	#[tauri::command]
2148	fn terminal_font_size() -> u32 {
2149	  fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
2150	    .ok()
2151	    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
2152	    .and_then(|v| v["terminalFontSize"].as_u64())
2153	    .map(|n| n as u32)
2154	    .unwrap_or(13)
2155	}
2156
2157	/// Setzt die Schriftgröße; erhält übrige App-settings.
2158	#[tauri::command]
2159	fn set_terminal_font_size(size: u32) -> Result<(), String> {
2160	  let paths = Paths::real();
2161	  let path = paths.config_dir().join(APP_SETTINGS_FILE);
2162	  let mut v: serde_json::Value = fs::read_to_string(&path)
2163	    .ok()
2164	    .and_then(|s| serde_json::from_str(&s).ok())
2165	    .unwrap_or_else(|| serde_json::json!({}));
2166	  v["terminalFontSize"] = serde_json::json!(size);
2167	  fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?;
2168	  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
2169	  fs::write(&path, raw + "\n").map_err(|e| format!("{}: {e}", path.display()))
2170	}
2171
2172	/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool —
2173	/// sonst würde das Transkript der laufenden Session ersetzt.
2174	#[tauri::command]
2175	fn link_pool_runtime(pool: String) -> Result<(), String> {
2176	  let paths = Paths::real();
2177	  if !running_projects_using_pool(&paths, &pool)?.is_empty() {
2178	    return Err(format!("{pool} wird benutzt — erst Session beenden"));
2179	  }
2180	  link_pool_runtime_in(&paths, &pool)
2181	}
2182
2183	#[cfg_attr(mobile, tauri::mobile_entry_point)]
2184	pub fn run() {

/// Terminal-Schriftgröße (settings.json: terminalFontSize), ein Wert für alle
/// Terminals. Default 13.
#[tauri::command]
fn terminal_font_size() -> u32 {
  fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .and_then(|v| v["terminalFontSize"].as_u64())
    .map(|n| n as u32)
    .unwrap_or(13)
}

/// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> {
  let paths = Paths::real();
  let path = paths.config_dir().join(APP_SETTINGS_FILE);
  let mut v: serde_json::Value = fs::read_to_string(&path)
    .ok()
    .and_then(|s| serde_json::from_str(&s).ok())
    .unwrap_or_else(|| serde_json::json!({}));
  v["terminalFontSize"] = serde_json::json!(size);
  fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?;
  let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
  fs::write(&path, raw + "\n").map_err(|e| format!("{}: {e}", path.display()))
}

/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool —
/// sonst würde das Transkript der laufenden Session ersetzt.
#[tauri::command]
fn link_pool_runtime(pool: String) -> Result<(), String> {
  let paths = Paths::real();
  if !running_projects_using_pool(&paths, &pool)?.is_empty() {
    return Err(format!("{pool} wird benutzt — erst Session beenden"));
  }
  link_pool_runtime_in(&paths, &pool)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {


Klar: Default in `settings.json` (`spellcheckLang`, initial „de"), plus ein Selector im Panel als per-Text-Override. Rust-Command zum Lesen des Defaults:


Edit

/// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> {

/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),
/// Default „de". Pro Text im Panel überschreibbar.
#[tauri::command]
fn spellcheck_lang() -> String {
  fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))
    .unwrap_or_else(|| "de".to_string())
}

/// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> {


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

/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),
/// Default „de". Pro Text im Panel überschreibbar.
#[tauri::command]
fn spellcheck_lang() -> String {
  fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))
    .unwrap_or_else(|| "de".to_string())
}

/// Setzt die Schriftgröße; erhält übrige App-settings.
#[tauri::command]
fn set_terminal_font_size(size: u32) -> Result<(), String> {

+/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),

+/// Default „de". Pro Text im Panel überschreibbar.

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

+    .and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))

+    .unwrap_or_else(|| "de".to_string())


      terminal_font_size,
      set_terminal_font_size
    ])

      terminal_font_size,
      set_terminal_font_size,
      spellcheck_lang
    ])


      terminal_font_size,
      set_terminal_font_size
    ])

      terminal_font_size,
      set_terminal_font_size,
      spellcheck_lang
    ])


Frontend: `panel-view.ts` um Sprach-Selector erweitern:


  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {

  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  langSelect?: HTMLSelectElement;
  /// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).
  defaultLang?: string;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {


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

  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  langSelect?: HTMLSelectElement;
  /// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).
  defaultLang?: string;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {

import { marked } from "marked";

export interface PanelView {
  set(text: string): void;
  raw(): string;
}

/// Verkabelt einen Panel-Inhaltsbereich: MD/Roh-Umschalter und Copy-Button.
/// Der Rohtext bleibt die Quelle für „Kopieren"; die Ansicht rendert Markdown.
/// Entfernt Inline-Markdown (Emphasis, Code, Links), damit der Plain-Text-Titel
/// nicht die rohen Marker zeigt.
function stripInlineMd(s: string): string {
  return s
    .replace(/`([^`]*)`/g, "$1")
    .replace(/\*\*([^*]+)\*\*/g, "$1")
    .replace(/\*([^*]+)\*/g, "$1")
    .replace(/__([^_]+)__/g, "$1")
    .replace(/_([^_]+)_/g, "$1")
    .replace(/~~([^~]+)~~/g, "$1")
    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
    .trim();
}

/// Titel für den Panel-Kopf: erste Überschrift (# …) oder sonst erste
/// nicht-leere Zeile, ohne Inline-Markdown; „Entwurf" bei leerem Text.
function firstLine(text: string): string {
  let fallback = "";
  for (const line of text.split("\n")) {
    const t = line.trim();
    if (!t) continue;
    const h = t.replace(/^#+/, "").trim();
    if (t.startsWith("#") && h) return stripInlineMd(h);
    if (!fallback) fallback = t;
  }
  return fallback ? stripInlineMd(fallback) : "Entwurf";
}

/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
function setHeading(text: string, title: string): string {
  const lines = text.split("\n");
  for (let i = 0; i < lines.length; i++) {
    const t = lines[i].trim();
    if (!t) continue;
    if (t.startsWith("#")) {
      const hashes = (t.match(/^#+/) as RegExpMatchArray)[0];
      lines[i] = `${hashes} ${title}`;
    } else {
      lines.splice(i, 0, `# ${title}`, "");
    }
    return lines.join("\n");
  }
  return `# ${title}\n`;
}

export function initPanelView(opts: {
  content: HTMLElement;
  copyBtn: HTMLElement;
  modeBtn: HTMLElement;
  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;
  // Content-Edit-Zustand: während des Editierens werden eingehende Updates
  // gepuffert statt angewandt.
  let editing = false;
  let pending: string | null = null;

  function draw() {
    if (rendered) {
      opts.content.className = "md";
      opts.content.innerHTML = marked.parse(rawText, { async: false });
    } else {
      opts.content.className = "raw";
      opts.content.textContent = rawText;
    }
    opts.modeBtn.textContent = rendered ? "MD" : "Roh";
  }

  opts.modeBtn.addEventListener("click", () => {
    rendered = !rendered;
    draw();
  });

  opts.copyBtn.addEventListener("click", async () => {
    await navigator.clipboard.writeText(rawText);
    opts.copyBtn.classList.add("copied");
    setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
  });

  // Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
  // geänderte Überschrift zurück, Escape verwirft.
  const titleEl = opts.titleEl;
  if (opts.editBtn && titleEl && opts.onCommit) {
    const commit = () => {
      if (titleEl.getAttribute("contenteditable") !== "true") return;
      titleEl.removeAttribute("contenteditable");
      const nt = (titleEl.textContent || "").trim();
      if (nt && nt !== firstLine(rawText)) opts.onCommit!(setHeading(rawText, nt));
      else titleEl.textContent = firstLine(rawText);
    };
    opts.editBtn.addEventListener("click", () => {
      titleEl.setAttribute("contenteditable", "true");
      titleEl.focus();
      const r = document.createRange();
      r.selectNodeContents(titleEl);
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(r);
    });
    titleEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        titleEl.blur();
      } else if (e.key === "Escape") {
        titleEl.textContent = firstLine(rawText);
        titleEl.removeAttribute("contenteditable");
        titleEl.blur();
      }
    });
    titleEl.addEventListener("blur", commit);
  }

  // Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
  // (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
  // Eingehende Updates während des Editierens werden gepuffert.
  if (opts.editContentBtn && opts.onCommit) {
    const editBtn = opts.editContentBtn;
    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.lang = "de";
    editor.hidden = true;
    opts.content.after(editor);

    const leave = () => {
      editing = false;
      editor.hidden = true;
      opts.content.hidden = false;
      editBtn.classList.remove("active", "changed");
    };
    const enter = () => {
      editing = true;
      pending = null;
      editor.value = rawText;
      opts.content.hidden = true;
      editor.hidden = false;
      editBtn.classList.add("active");
      editor.focus();
    };
    const save = () => {
      const v = editor.value;
      leave();
      opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
    };
    const cancel = () => {
      const p = pending;
      leave();
      if (p !== null) applyText(p);
      else draw();
    };
    editBtn.addEventListener("click", () => (editing ? save() : enter()));
    editor.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        save();
      } else if (e.key === "Escape") {
        e.preventDefault();
        cancel();
      }
    });
  }

  function applyText(text: string) {
    rawText = text;
    if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
      titleEl.textContent = firstLine(text);
    }
    draw();
  }

  draw();
  return {
    set(text: string) {
      // Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den
      // Edit-Button als „geändert" markieren.
      if (editing) {
        pending = text;
        opts.editContentBtn?.classList.add("changed");
        return;
      }
      applyText(text);
    },
    raw: () => rawText,
  };
}

+  /// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).


    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.lang = "de";
    editor.hidden = true;
    opts.content.after(editor);

    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.hidden = true;
    opts.content.after(editor);

    // Sprache der Rechtschreibprüfung: Default aus den Settings, per Selector
    // pro Text überschreibbar.
    let lang = opts.defaultLang || "de";
    const sel = opts.langSelect;
    if (sel) {
      if ([...sel.options].some((o) => o.value === lang)) sel.value = lang;
      else lang = sel.value;
      sel.addEventListener("change", () => {
        lang = sel.value;
        editor.lang = lang;
        // Neuprüfung erzwingen.
        editor.spellcheck = false;
        editor.spellcheck = true;
        if (!editor.hidden) editor.focus();
      });
    }
    editor.lang = lang;


    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.hidden = true;
    opts.content.after(editor);

    // Sprache der Rechtschreibprüfung: Default aus den Settings, per Selector
    // pro Text überschreibbar.
    let lang = opts.defaultLang || "de";
    const sel = opts.langSelect;
    if (sel) {
      if ([...sel.options].some((o) => o.value === lang)) sel.value = lang;
      else lang = sel.value;
      sel.addEventListener("change", () => {
        lang = sel.value;
        editor.lang = lang;
        // Neuprüfung erzwingen.
        editor.spellcheck = false;
        editor.spellcheck = true;
        if (!editor.hidden) editor.focus();
      });
    }
    editor.lang = lang;

import { marked } from "marked";

export interface PanelView {
  set(text: string): void;
  raw(): string;
}

/// Verkabelt einen Panel-Inhaltsbereich: MD/Roh-Umschalter und Copy-Button.
/// Der Rohtext bleibt die Quelle für „Kopieren"; die Ansicht rendert Markdown.
/// Entfernt Inline-Markdown (Emphasis, Code, Links), damit der Plain-Text-Titel
/// nicht die rohen Marker zeigt.
function stripInlineMd(s: string): string {
  return s
    .replace(/`([^`]*)`/g, "$1")
    .replace(/\*\*([^*]+)\*\*/g, "$1")
    .replace(/\*([^*]+)\*/g, "$1")
    .replace(/__([^_]+)__/g, "$1")
    .replace(/_([^_]+)_/g, "$1")
    .replace(/~~([^~]+)~~/g, "$1")
    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
    .trim();
}

/// Titel für den Panel-Kopf: erste Überschrift (# …) oder sonst erste
/// nicht-leere Zeile, ohne Inline-Markdown; „Entwurf" bei leerem Text.
function firstLine(text: string): string {
  let fallback = "";
  for (const line of text.split("\n")) {
    const t = line.trim();
    if (!t) continue;
    const h = t.replace(/^#+/, "").trim();
    if (t.startsWith("#") && h) return stripInlineMd(h);
    if (!fallback) fallback = t;
  }
  return fallback ? stripInlineMd(fallback) : "Entwurf";
}

/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
function setHeading(text: string, title: string): string {
  const lines = text.split("\n");
  for (let i = 0; i < lines.length; i++) {
    const t = lines[i].trim();
    if (!t) continue;
    if (t.startsWith("#")) {
      const hashes = (t.match(/^#+/) as RegExpMatchArray)[0];
      lines[i] = `${hashes} ${title}`;
    } else {
      lines.splice(i, 0, `# ${title}`, "");
    }
    return lines.join("\n");
  }
  return `# ${title}\n`;
}

export function initPanelView(opts: {
  content: HTMLElement;
  copyBtn: HTMLElement;
  modeBtn: HTMLElement;
  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  editContentBtn?: HTMLElement;
  langSelect?: HTMLSelectElement;
  /// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).
  defaultLang?: string;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
  /// wurden.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;
  // Content-Edit-Zustand: während des Editierens werden eingehende Updates
  // gepuffert statt angewandt.
  let editing = false;
  let pending: string | null = null;

  function draw() {
    if (rendered) {
      opts.content.className = "md";
      opts.content.innerHTML = marked.parse(rawText, { async: false });
    } else {
      opts.content.className = "raw";
      opts.content.textContent = rawText;
    }
    opts.modeBtn.textContent = rendered ? "MD" : "Roh";
  }

  opts.modeBtn.addEventListener("click", () => {
    rendered = !rendered;
    draw();
  });

  opts.copyBtn.addEventListener("click", async () => {
    await navigator.clipboard.writeText(rawText);
    opts.copyBtn.classList.add("copied");
    setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
  });

  // Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
  // geänderte Überschrift zurück, Escape verwirft.
  const titleEl = opts.titleEl;
  if (opts.editBtn && titleEl && opts.onCommit) {
    const commit = () => {
      if (titleEl.getAttribute("contenteditable") !== "true") return;
      titleEl.removeAttribute("contenteditable");
      const nt = (titleEl.textContent || "").trim();
      if (nt && nt !== firstLine(rawText)) opts.onCommit!(setHeading(rawText, nt));
      else titleEl.textContent = firstLine(rawText);
    };
    opts.editBtn.addEventListener("click", () => {
      titleEl.setAttribute("contenteditable", "true");
      titleEl.focus();
      const r = document.createRange();
      r.selectNodeContents(titleEl);
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(r);
    });
    titleEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        titleEl.blur();
      } else if (e.key === "Escape") {
        titleEl.textContent = firstLine(rawText);
        titleEl.removeAttribute("contenteditable");
        titleEl.blur();
      }
    });
    titleEl.addEventListener("blur", commit);
  }

  // Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
  // (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
  // Eingehende Updates während des Editierens werden gepuffert.
  if (opts.editContentBtn && opts.onCommit) {
    const editBtn = opts.editContentBtn;
    const editor = document.createElement("textarea");
    editor.className = "panel-editor";
    editor.spellcheck = true;
    editor.lang = "de";
    editor.hidden = true;
    opts.content.after(editor);

    const leave = () => {
      editing = false;
      editor.hidden = true;
      opts.content.hidden = false;
      editBtn.classList.remove("active", "changed");
    };
    const enter = () => {
      editing = true;
      pending = null;
      editor.value = rawText;
      opts.content.hidden = true;
      editor.hidden = false;
      editBtn.classList.add("active");
      editor.focus();
    };
    const save = () => {
      const v = editor.value;
      leave();
      opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
    };
    const cancel = () => {
      const p = pending;
      leave();
      if (p !== null) applyText(p);
      else draw();
    };
    editBtn.addEventListener("click", () => (editing ? save() : enter()));
    editor.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        save();
      } else if (e.key === "Escape") {
        e.preventDefault();
        cancel();
      }
    });
  }

  function applyText(text: string) {
    rawText = text;
    if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
      titleEl.textContent = firstLine(text);
    }
    draw();
  }

  draw();
  return {
    set(text: string) {
      // Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den
      // Edit-Button als „geändert" markieren.
      if (editing) {
        pending = text;
        opts.editContentBtn?.classList.add("changed");
        return;
      }
      applyText(text);
    },
    raw: () => rawText,
  };
}

+    // Sprache der Rechtschreibprüfung: Default aus den Settings, per Selector

+    let lang = opts.defaultLang || "de";

+      if ([...sel.options].some((o) => o.value === lang)) sel.value = lang;

+      sel.addEventListener("change", () => {

+        if (!editor.hidden) editor.focus();


HTML + CSS: Sprach-Selector in die Aktionszeile. `terminal.html`:


      .panel-editor[hidden] {
        display: none;
      }

      .panel-editor[hidden] {
        display: none;
      }
      .panel-lang {
        margin-right: auto;
        background: #313244;
        color: #a6adc8;
        border: none;
        border-radius: 6px;
        padding: 3px 4px;
        font: inherit;
        font-size: 11px;
        cursor: default;
      }


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

      .panel-editor[hidden] {
        display: none;
      }
      .panel-lang {
        margin-right: auto;
        background: #313244;
        color: #a6adc8;
        border: none;
        border-radius: 6px;
        padding: 3px 4px;
        font: inherit;
        font-size: 11px;
        cursor: default;
      }

.panel-editor[hidden] {
display: none;
}
+      .panel-lang {
+        margin-right: auto;
+        background: #313244;
+        color: #a6adc8;
+        border: none;
+        border-radius: 6px;
+        padding: 3px 4px;
+        font: inherit;
+        font-size: 11px;
+        cursor: default;
+      }
#panel-content {
flex: 1;
min-height: 0;


          <div class="panel-actions">
            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

          <div class="panel-actions">
            <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
              <option value="de">DE</option>
              <option value="en">EN</option>
              <option value="en-GB">EN-GB</option>
              <option value="fr">FR</option>
              <option value="es">ES</option>
              <option value="it">IT</option>
              <option value="nl">NL</option>
            </select>
            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>


          <div class="panel-actions">
            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

          <div class="panel-actions">
            <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
              <option value="de">DE</option>
              <option value="en">EN</option>
              <option value="en-GB">EN-GB</option>
              <option value="fr">FR</option>
              <option value="es">ES</option>
              <option value="it">IT</option>
              <option value="nl">NL</option>
            </select>
            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>

+            <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">

+              <option value="de">DE</option>

+              <option value="en">EN</option>

+              <option value="en-GB">EN-GB</option>

+              <option value="fr">FR</option>

+              <option value="es">ES</option>

+              <option value="it">IT</option>

+              <option value="nl">NL</option>

<button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">


`panel.html` genauso:


      .panel-editor[hidden] {
        display: none;
      }

      .panel-editor[hidden] {
        display: none;
      }
      .panel-lang {
        margin-right: auto;
        background: #313244;
        color: #a6adc8;
        border: none;
        border-radius: 6px;
        padding: 3px 4px;
        font: inherit;
        font-size: 11px;
        cursor: default;
      }


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

<!doctype html>

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

        Entwurf
        <button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>

        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
        <button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
          <svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
        </button>
        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">
          <svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
        </button>
        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">
          <svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
        </button>
        <button class="panel-btn" id="panel-dock" title="Wieder andocken">
          <svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5" /><path d="M9.5 2.5v11" /><path d="M4 8h3.4M6 6.2 7.8 8 6 9.8" /></svg>
        </button>
        <button class="panel-btn" id="panel-close" title="Panel schließen">
          <svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
        </button>


      <div class="panel-actions">
        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

      <div class="panel-actions">
        <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
          <option value="de">DE</option>
          <option value="en">EN</option>
          <option value="en-GB">EN-GB</option>
          <option value="fr">FR</option>
          <option value="es">ES</option>
          <option value="it">IT</option>
          <option value="nl">NL</option>
        </select>
        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>


      <div class="panel-actions">
        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

      <div class="panel-actions">
        <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
          <option value="de">DE</option>
          <option value="en">EN</option>
          <option value="en-GB">EN-GB</option>
          <option value="fr">FR</option>
          <option value="es">ES</option>
          <option value="it">IT</option>
          <option value="nl">NL</option>
        </select>
        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>

+        <select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">

+          <option value="de">DE</option>

+          <option value="en">EN</option>

+          <option value="en-GB">EN-GB</option>

+          <option value="fr">FR</option>

+          <option value="es">ES</option>

+          <option value="it">IT</option>

+          <option value="nl">NL</option>


Verkabelung in `terminal.ts` und `panel.ts` — Default lesen und übergeben:


  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  onCommit: (text) => invoke("panel_set", { project, text }),
});

  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
  defaultLang: await invoke<string>("spellcheck_lang"),
  onCommit: (text) => invoke("panel_set", { project, text }),
});


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

  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  onCommit: (text) => invoke("panel_set", { project, text }),
});

  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
  defaultLang: await invoke<string>("spellcheck_lang"),
  onCommit: (text) => invoke("panel_set", { project, text }),
});

titleEl: panel.querySelector(".panel-title") as HTMLElement,

+  langSelect: document.getElementById("panel-lang") as HTMLSelectElement,

+  defaultLang: await invoke<string>("spellcheck_lang"),


  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  onCommit: (text) => invoke("panel_set", { project, text }),
});

  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
  defaultLang: await invoke<string>("spellcheck_lang"),
  onCommit: (text) => invoke("panel_set", { project, text }),
});


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

import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { listen, emit } from "@tauri-apps/api/event";
import { initPanelView } from "./panel-view";

// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
// danach denselben `panel-update`-Events wie das angedockte Panel.
const project = new URLSearchParams(location.search).get("project")!;
const win = getCurrentWebviewWindow();

const view = initPanelView({
  content: document.getElementById("panel-content")!,
  copyBtn: document.getElementById("panel-copy")!,
  modeBtn: document.getElementById("panel-mode")!,
  titleEl: document.querySelector(".panel-title") as HTMLElement,
  editBtn: document.getElementById("panel-title-edit")!,
  editContentBtn: document.getElementById("panel-content-edit")!,
  onCommit: (text) => invoke("panel_set", { project, text }),
});

view.set(await invoke<string>("panel_read", { project }));

await listen<string>("panel-update", (e) => view.set(e.payload));

// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
const archiveBtn = document.getElementById("panel-archive")!;
function flashBtn(btn: HTMLElement, cls: string) {
  btn.classList.add(cls);
  setTimeout(() => btn.classList.remove(cls), 1400);
}
archiveBtn.addEventListener("click", async () => {
  try {
    const configured = await invoke<string | null>("panel_archive_dir_cmd", {
      project,
    });
    let dir: string | undefined;
    if (!configured) {
      const { open } = await import("@tauri-apps/plugin-dialog");
      const chosen = await open({ directory: true, title: "Archiv-Ordner wählen" });
      if (!chosen) return;
      dir = chosen as string;
    }
    const path = await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
    invoke("reveal_path_cmd", { path });
  } catch {
    flashBtn(archiveBtn, "error");
  }
});

// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
document.getElementById("panel-dock")!.addEventListener("click", async () => {
  await emit("panel-attached");
  win.close();
});

// „Schließen": Fenster zu, ohne wieder anzudocken (Panel bleibt aus, bis ein
// neuer Entwurf kommt).
document.getElementById("panel-close")!.addEventListener("click", () => win.close());


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup"; export PATH="$CARGO_HOME/bin:$PATH"; cd ~/projects/ai-control && npm run build 2>&1 | grep -E "error|Error|✓ built" | head && cd src-tauri && cargo build 2>&1 | grep -E "error|warning:|Finished" | head && printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | true

Frontend + Rust bauen


✓ built in 268ms
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.94s


Beide grün. Kurz prüfen, dass `spellcheck_lang` im Terminal-Prozess-Handler registriert ist (dort läuft das Panel):


cd ~/projects/ai-control/src-tauri/src; awk '/fn terminal_builder/,/^}/' lib.rs | grep -n "spellcheck_lang\|generate_handler\|panel_set\|panel_archive"

spellcheck_lang im terminal_builder prüfen


23:    .invoke_handler(tauri::generate_handler![
29:      terminal::panel_set,
31:      panel_archive_dir_cmd,
32:      panel_archive_cmd,
40:      spellcheck_lang


Gebaut, alles grün. `spellcheck_lang` liegt korrekt im Terminal-Prozess-Handler.

**Rechtschreibprüfung, zwei Ebenen:**
- **Default** aus der App-`settings.json` (`spellcheckLang`, initial „de") — gilt für neue Panels. Änderbar per Hand in der settings.json (eine Settings-UI dafür kann später kommen).
- **Pro Text** ein kleiner Selector links in der Button-Zeile (DE / EN / EN-GB / FR / ES / IT / NL). Er startet beim Default und überschreibt die Sprache für den aktuellen Entwurf; beim Wechsel wird die Prüfung neu ausgelöst. Der Override gilt sessionweise, nicht persistiert.

Angedockt und abgelöst. Die eigentliche Prüfung macht die Webview nativ (macOS out of the box; Linux WebKitGTK-abhängig — beim Umzug testen).

**Geändert:** `lib.rs` (+`spellcheck_lang`), `panel-view.ts`, `terminal.ts`, `panel.ts`, `terminal.html`, `panel.html`.

Nichts committet. Damit sind die Items durch und der Rechtschreib-Zusatz drin — bereit zum Commit für den Linux-Umzug.


alles commiten und psuhen
kannszt diu den Imstieg auf Linux vorbereiten?
da checken wir alles noch mal, und machen ein Relaese
hattest du die Readme angepasst?


Erst committen + pushen, dann Linux-Vorbereitung und README-Status.


cd ~/projects/ai-control && git add -A && git status -s && echo "---" && git diff --cached --stat | tail -3

Stagen und prüfen


M  panel.html
M  src-tauri/src/lib.rs
M  src/panel-view.ts
M  src/panel.ts
M  src/terminal.ts
M  terminal.html
---
 src/terminal.ts      |  25 ++++++++-----
 terminal.html        |  73 ++++++++++++++++++++++++++++++++++--
 6 files changed, 336 insertions(+), 56 deletions(-)


cd ~/projects/ai-control && git commit -q -m "Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache

- register_mcp_server schreibt .claude.json nur bei fehlendem/abweichendem
  Eintrag (claudes Live-State nicht unnötig überschreiben).
- Content-Editor im Panel (Rohtext-Textarea): Cmd/Ctrl+Enter speichert via
  panel_set, Esc verwirft; eingehende Updates werden während der Bearbeitung
  gepuffert (Button „geändert\"), Speichern gewinnt.
- Archivieren zeigt die Datei danach im Dateimanager (reveal_path_cmd, cfg pro
  OS). Breadth-Guard: Archiv-Ordner darf kein Home-Vorfahre sein, muss absolut
  sein; expand_home löst bare ~ auf.
- Rechtschreibprüfung: Default in settings.json (spellcheckLang, „de"), pro
  Text per Selector überschreibbar." && git log --oneline -1 && echo "=== push ===" && git push origin main 2>&1 | tail -3

Commit und Push


Error: Exit code 1
(eval):11: parse error near `)'


Shell-Parsefehler durch ein gerades `"` in der Message. Ich committe per Heredoc:


cd ~/projects/ai-control && git commit -q -F - <<'MSG' && git log --oneline -1 && echo "=== push ===" && git push origin main 2>&1 | tail -3
Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache

- register_mcp_server schreibt .claude.json nur bei fehlendem/abweichendem
  Eintrag (claudes Live-State nicht unnötig ueberschreiben).
- Content-Editor im Panel (Rohtext-Textarea): Cmd/Ctrl+Enter speichert via
  panel_set, Esc verwirft; eingehende Updates werden waehrend der Bearbeitung
  gepuffert (Button "geaendert"), Speichern gewinnt.
- Archivieren zeigt die Datei danach im Dateimanager (reveal_path_cmd, cfg pro
  OS). Breadth-Guard: Archiv-Ordner darf kein Home-Vorfahre sein, muss absolut
  sein; expand_home loest bare ~ auf.
- Rechtschreibpruefung: Default in settings.json (spellcheckLang "de"), pro
  Text per Selector ueberschreibbar.
MSG

Commit per Heredoc und Push


165501c Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache
=== push ===
remote: Processed 1 references in total
To ssh://git.lintus.art:2222/marcus.hinz/ai-control.git
   e2e9992..165501c  main -> main


alles commiten und psuhen kannszt diu den Imstieg auf Linux vorbereiten? da checken wir alles noch mal, und machen ein Relaese hattest du die Readme angepasst?


Committet + gepusht (`e2e9992..165501c`). Jetzt README lesen und den neuen Code auf Linux-Tauglichkeit (cfg) prüfen:


1	# ai-control
2
3	Pool and session management for [Claude Code](https://claude.com/claude-code) on macOS — a tray app with a built-in terminal. Linux and Windows integration to follow.
4
5	> All code in this project was generated by Claude (Claude Code).
6
7	<img src="docs/projects.png" width="640" alt="Projects">
8
9	## Purpose
10
11	Running Claude Code with multiple accounts or credential sets means hitting the right `CLAUDE_CONFIG_DIR` (plus keychain entry) for every session. ai-control turns that into clicks:
12
13	- **Pools** — named credential sets (OAuth login or API key) under `~/.config/ai-control/pools/<pool>`. Each pool is a self-contained Claude config directory with its own keychain entry.
14	- **Projects** — arbitrary directories, mapped through a registry (`~/.config/ai-control/projects.json`, name → path; paths under home are stored as `~/…` and therefore stable across machines). Each project is assigned a pool, plus terminal settings (theme, icon, window title).
15	- **Sessions** — a built-in terminal per project (xterm.js + PTY) that launches Claude Code with the pool's config directory as `CLAUDE_CONFIG_DIR`. Every terminal runs as its own process with its own Dock icon and Cmd-Tab entry.
16	- **Tray** — the app itself is a pure menu-bar app without a Dock entry. The tray menu lists all projects with icon and status dot (green = running); clicking starts the project or brings the running terminal to the front.
17	- **Session watcher** — detects the end of a session by the terminal process disappearing and then (opt-in) syncs the Git repository containing the project: add → commit → pull --rebase → push.
18
19	## Screenshots
20
21	| Pools | Usage |
22	|---|---|
23	| ![Pools](docs/pools.png) | ![Usage](docs/usage.png) |
24
25	| Tray menu | Session terminal |
26	|---|---|
27	| ![Tray menu](docs/tray.png) | ![Session terminal](docs/terminal.png) |
28
29	## Project layout
30
31	```
32	├── index.html            main UI (project/pool management)
33	├── terminal.html         terminal window (xterm.js)
34	├── src/                  frontend: Vue 3 + TypeScript
35	├── src-tauri/
36	│   ├── src/lib.rs        Tauri commands: projects, pools, keychain,
37	│   │                     OAuth login, tray menu, session watcher
38	│   ├── src/terminal.rs   PTY sessions (portable-pty), terminal windows,
39	│   │                     Dock icon, focusing running terminals
40	│   └── icons/            app and tray icons
41	├── dev.sh                development mode (tauri dev)
42	└── build.sh              release build (.app bundle)
43	```
44
45	Two process roles from one binary:
46
47	- **Main app** (`ai-control`) — tray, main window, management, watcher.
48	- **Terminal process** (`ai-control --terminal <project>`) — one window with its own PTY; launches Claude Code in the project directory with the assigned pool's `CLAUDE_CONFIG_DIR`.
49
50	Running projects are detected through their terminal processes (`pgrep` for `--terminal <project>`), without a state file.
51
52	## Tooling
53
54	| Area      | Stack |
55	|-----------|-------|
56	| Shell     | Tauri 2 (Rust) |
57	| Frontend  | Vue 3, TypeScript, Vite |
58	| Terminal  | xterm.js, portable-pty |
59	| macOS     | objc2 / objc2-app-kit (Dock icon, window focus, tray) |
60	| Secrets   | keyring (macOS Keychain) |
61
62	## Installation
63
64	Prebuilt DMGs are attached to the releases. The bundles are **not signed or notarized** — an Apple Developer ID costs €99/year, which this project doesn't have. If you'd like to change that: [Buy me a coffee](https://buymeacoffee.com/marcusH). macOS quarantines the unsigned download; after copying `ai-control.app` to `~/Applications`, clear it with:
65
66	```sh
67	xattr -d com.apple.quarantine ~/Applications/ai-control.app
68	```
69
70	Or avoid the topic entirely and build from source (below) — self-built apps aren't quarantined.
71
72	## Development
73
74	```sh
75	./dev.sh      # tauri dev with hot reload
76	./build.sh    # release build; bundle lands in src-tauri/target/release/bundle/macos.noindex/
77	```
78
79	`build.sh` moves the bundle into a `.noindex` directory and unregisters it from Launch Services so Spotlight only finds the installed copy. Install by copying `ai-control.app` to `~/Applications`.
80
81	Prerequisites: Rust (stable), Node.js/npm, macOS. `dev.sh`/`build.sh` expect `CARGO_HOME`/`RUSTUP_HOME` under `~/tools/` — adjust the two exports in the scripts for a standard installation.
82
83	## Configuration layout
84
85	```
86	~/.config/ai-control/
87	├── settings.json                 app settings (see below)
88	├── projects.json                 project registry: name → directory
89	└── pools/<pool>/                 one Claude config directory per pool
90	    └── pool.json                 name + credential type
91
92	<project directory>/              any path, mapped via the registry
93	├── .claude/                      Claude Code project configuration
94	└── ai-control.json               pool assignment + terminal settings
95	```
96
97	### settings.json
98
99	| Key                | Default  | Meaning |
100	|--------------------|----------|---------|
101	| `claudeCommand`    | `claude` | Command launched in the project terminal (via `zsh -ic`, so your `.zshrc` PATH applies). |
102	| `syncOnSessionEnd` | `false`  | After a session ends, commit and push the Git repository containing the project. |
103	| `poolSyncDir`      | unset    | Directory to sync pool runtime data into (session transcripts, todos, prompt history — what `/resume` needs). When set, a pool's runtime files are replaced by symlinks into `<poolSyncDir>/<pool>`, so sessions travel with however that directory is synced (git, Syncthing, …). Unset: everything stays local in the pool directory. |
104
105	## Claude and Anthropic licensing
106
107	ai-control is pool management only — it does not change, extend, or circumvent Anthropic's licensing in any way. Using Claude Code through ai-control is subject to the same Anthropic terms as running it directly.
108
109	For that reason the app stores no OAuth tokens itself: with an OAuth pool, the first session of a project on that pool goes through the regular Anthropic login (`/login`, browser, against your subscription), run by Claude Code itself. Credentials live where Claude Code puts them — in the macOS keychain, one entry per pool.
110
111	API keys go into the keychain as well (one entry per pool). Only when no keychain/keyring is available does the app offer — after an explicit confirmation — to store the key as a file in the pool directory (mode 0600). We strongly advise against that fallback: an API key on disk is readable by every process running as your user. Use the keychain.
112
113	## License
114
115	[MIT](LICENSE)
116

# ai-control

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

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

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

## Purpose

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

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

## Screenshots

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

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

## Project layout

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

Two process roles from one binary:

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

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

## Tooling

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

## Installation

Prebuilt DMGs are attached to the releases. The bundles are **not signed or notarized** — an Apple Developer ID costs €99/year, which this project doesn't have. If you'd like to change that: [Buy me a coffee](https://buymeacoffee.com/marcusH). macOS quarantines the unsigned download; after copying `ai-control.app` to `~/Applications`, clear it with:

```sh
xattr -d com.apple.quarantine ~/Applications/ai-control.app
```

Or avoid the topic entirely and build from source (below) — self-built apps aren't quarantined.

## Development

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

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

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

## Configuration layout

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

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

### settings.json

| Key                | Default  | Meaning |
|--------------------|----------|---------|
| `claudeCommand`    | `claude` | Command launched in the project terminal (via `zsh -ic`, so your `.zshrc` PATH applies). |
| `syncOnSessionEnd` | `false`  | After a session ends, commit and push the Git repository containing the project. |
| `poolSyncDir`      | unset    | Directory to sync pool runtime data into (session transcripts, todos, prompt history — what `/resume` needs). When set, a pool's runtime files are replaced by symlinks into `<poolSyncDir>/<pool>`, so sessions travel with however that directory is synced (git, Syncthing, …). Unset: everything stays local in the pool directory. |

## Claude and Anthropic licensing

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

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

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

## License

[MIT](LICENSE)


README gelesen. Kurz-Audit des neuen Codes auf un-cfg'te macOS-Aufrufe:


cd ~/projects/ai-control/src-tauri/src
echo "=== Command::new-Aufrufe (Plattform-relevant) ==="
grep -n 'Command::new' lib.rs terminal.rs mcp.rs
echo "=== macos-cfg-Blöcke ohne linux-Pendant? (reveal_path prüfen) ==="
grep -n 'fn reveal_path\|target_os = "macos"\|target_os = "linux"\|target_os = "windows"' lib.rs | grep -i reveal -A0 || grep -n 'reveal_path' lib.rs

cfg-Audit des neuen Codes


=== Command::new-Aufrufe (Plattform-relevant) ===
lib.rs:375:  let out = Command::new("pgrep")
lib.rs:454:    Command::new("git")
lib.rs:1114:  let out = Command::new("sips")
lib.rs:1181:    let out = Command::new("security")
lib.rs:1194:    let out = Command::new("security")
lib.rs:1203:    Command::new("security")
lib.rs:2055:  let out = Command::new("security")
lib.rs:2085:    let out = Command::new("security")
lib.rs:2101:    let out = Command::new("kill")
lib.rs:2308:  let _ = Command::new("open").arg("-R").arg(path).spawn();
lib.rs:2314:  let _ = Command::new("xdg-open").arg(target).spawn();
lib.rs:2319:  let _ = Command::new("explorer")
terminal.rs:34:  std::process::Command::new(exe)
=== macos-cfg-Blöcke ohne linux-Pendant? (reveal_path prüfen) ===
2302:fn reveal_path_cmd(path: String) {
2307:fn reveal_path(path: &std::path::Path) {
2312:fn reveal_path(path: &std::path::Path) {
2318:fn reveal_path(path: &std::path::Path) {


Mein neuer Code ist cfg-sauber (`reveal_path` hat macOS/Linux/Windows-Zweige). `pgrep`/`security`/`sips`/`kill` sind **bestehende** macOS-Aufrufe — der breitere Linux-Port (v. a. `sips` für Icon-Resize) steht unabhängig an. Ich lege den Linux-Handoff als Projekt-Memory ab:


Write

---
name: linux-release-prep
description: Stand vorm Linux-Umzug — Panel/MCP-Feature auf origin; Verifikations-Checkliste + offene Linux-Port-Lücken
metadata:
  type: project
---

Stand 2026-07-11: Das Entwurfs-Panel samt MCP-Tool ist fertig und auf `origin` (lintus), letzter Commit `165501c`. Auf der Linux-Box `origin` pullen, alles verifizieren, dann Release. Build-Deps + Release-Schritte stehen im Skill [[git-commit]] (Abschnitt 3): `webkit2gtk-4.1`, `libgtk-3`, `libayatana-appindicator3`, `librsvg2`, `patchelf`; `npm run tauri build`; `gh release`.

**Feature-Kurzfassung:** MCP-stdio-Server `app --mcp-panel` (Server-Key `text-panel`, Tool `write_panel` + `archive_panel`), pro Pool in `.claude.json` (`alwaysLoad`) + Freigabe `mcp__text-panel__write_panel` in `settings.json`, provisioniert beim App-Start. Panel-UI (`terminal.html`/`panel.html`, andockbar/ablösbar), Titel aus erster Überschrift + Titel-Edit, Content-Editor, Archiv pro Projekt (`archiveDir` + `additionalDirectories`), Rechtschreib-Sprache (`spellcheckLang`, per-Text-Selector).

**Auf Linux gezielt prüfen (WebKitGTK/GTK-abhängig):**
- MCP: claude startet `app --mcp-panel` als Kind, `write_panel` schreibt `AI_CONTROL_PANEL`, Watcher zieht es ins Panel; Tool ohne Rückfrage (Freigabe geladen).
- Ordner-Dialog (`tauri-plugin-dialog`) beim Archivieren.
- Rechtschreibprüfung in WebKitGTK: braucht enchant + Wörterbücher (z. B. `hunspell-de`), sonst keine Unterringelung.
- `reveal_path` nutzt `xdg-open` (öffnet den Ordner, kein „select" wie macOS).
- Abgelöstes Panel-Fenster: auf Linux baut es mit Default-Dekorationen (die Terminal-Fenster sind `decorations(false)`); prüfen, dass es sinnvoll aussieht.
- Zwei-Zeilen-Kopf + SVG-Icons rendern.

**Offene Linux-Port-Lücke (unabhängig vom Panel):** `sips` (Icon-Resize, lib.rs ~1114) ist macOS-only und braucht einen Linux-Pfad. `security`-Aufrufe sind der apiKeyHelper (bereits cfg-verzweigt). Siehe [[linux-umzug]].

**README:** komplett außer sync (nur macOS, kein Panel/MCP, keine Linux/Windows-Angaben, fehlende settings-Keys `terminalFontSize`/`spellcheckLang`). Beim Release überarbeiten — Framing (Linux/Windows „supported" vs. „to follow") hängt am Verifikations-Ergebnis auf der Box.


File created successfully at: /Users/marcus.hinz/claude-projects/ai-control/memory/linux-release-prep.md (file state is current in your context — no need to Read it back)

---
name: linux-release-prep
description: Stand vorm Linux-Umzug — Panel/MCP-Feature auf origin; Verifikations-Checkliste + offene Linux-Port-Lücken
metadata:
  node_type: memory
  type: project
  originSessionId: f7704328-4eb4-4f0f-bf42-da68abab401d
---

Stand 2026-07-11: Das Entwurfs-Panel samt MCP-Tool ist fertig und auf `origin` (lintus), letzter Commit `165501c`. Auf der Linux-Box `origin` pullen, alles verifizieren, dann Release. Build-Deps + Release-Schritte stehen im Skill [[git-commit]] (Abschnitt 3): `webkit2gtk-4.1`, `libgtk-3`, `libayatana-appindicator3`, `librsvg2`, `patchelf`; `npm run tauri build`; `gh release`.

**Feature-Kurzfassung:** MCP-stdio-Server `app --mcp-panel` (Server-Key `text-panel`, Tool `write_panel` + `archive_panel`), pro Pool in `.claude.json` (`alwaysLoad`) + Freigabe `mcp__text-panel__write_panel` in `settings.json`, provisioniert beim App-Start. Panel-UI (`terminal.html`/`panel.html`, andockbar/ablösbar), Titel aus erster Überschrift + Titel-Edit, Content-Editor, Archiv pro Projekt (`archiveDir` + `additionalDirectories`), Rechtschreib-Sprache (`spellcheckLang`, per-Text-Selector).

**Auf Linux gezielt prüfen (WebKitGTK/GTK-abhängig):**
- MCP: claude startet `app --mcp-panel` als Kind, `write_panel` schreibt `AI_CONTROL_PANEL`, Watcher zieht es ins Panel; Tool ohne Rückfrage (Freigabe geladen).
- Ordner-Dialog (`tauri-plugin-dialog`) beim Archivieren.
- Rechtschreibprüfung in WebKitGTK: braucht enchant + Wörterbücher (z. B. `hunspell-de`), sonst keine Unterringelung.
- `reveal_path` nutzt `xdg-open` (öffnet den Ordner, kein „select" wie macOS).
- Abgelöstes Panel-Fenster: auf Linux baut es mit Default-Dekorationen (die Terminal-Fenster sind `decorations(false)`); prüfen, dass es sinnvoll aussieht.
- Zwei-Zeilen-Kopf + SVG-Icons rendern.

**Offene Linux-Port-Lücke (unabhängig vom Panel):** `sips` (Icon-Resize, lib.rs ~1114) ist macOS-only und braucht einen Linux-Pfad. `security`-Aufrufe sind der apiKeyHelper (bereits cfg-verzweigt). Siehe [[linux-umzug]].

**README:** komplett außer sync (nur macOS, kein Panel/MCP, keine Linux/Windows-Angaben, fehlende settings-Keys `terminalFontSize`/`spellcheckLang`). Beim Release überarbeiten — Framing (Linux/Windows „supported" vs. „to follow") hängt am Verifikations-Ergebnis auf der Box.


Edit

- [Linux-Umzug](linux-umzug.md) — ab 2026-07-05 laufen die Sessions auf der Linux-Box; Start per start-claude.sh (Einzeiler, Pool private); macOS-Code muss noch hinter cfg(target_os)

- [Linux-Umzug](linux-umzug.md) — ab 2026-07-05 laufen die Sessions auf der Linux-Box; Start per start-claude.sh (Einzeiler, Pool private); macOS-Code muss noch hinter cfg(target_os)
- [Linux-Release-Prep](linux-release-prep.md) — Panel/MCP-Feature auf origin (165501c); Verifikations-Checkliste für Linux + offene Port-Lücke (sips); README außer sync, beim Release überarbeiten


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

- [Linux-Umzug](linux-umzug.md) — ab 2026-07-05 laufen die Sessions auf der Linux-Box; Start per start-claude.sh (Einzeiler, Pool private); macOS-Code muss noch hinter cfg(target_os)
- [Linux-Release-Prep](linux-release-prep.md) — Panel/MCP-Feature auf origin (165501c); Verifikations-Checkliste für Linux + offene Port-Lücke (sips); README außer sync, beim Release überarbeiten

# Memory Index

- [Go-Pflicht](feedback-go-pflicht.md) — weitreichende Änderungen nie ohne ausdrückliches Go; eine Rückfrage von Marcus ist kein Go (Vorfall 2026-07-04)
- [Terminal-Tests mit 08-07](feedback-terminal-test-08-07.md) — ABSOLUTES VERBOT: nie ein Terminal für/aus ai-control starten (killt die eigene Session, 3× Kontext verloren am 2026-07-04). Terminal-Tests nur mit 08-07 und nur auf ausdrückliche Ansage
- [Nicht suchen, fragen](feedback-nicht-suchen-fragen.md) — fehlt ein Tool/Pfad: fragen statt find/ls/which; cargo/rustc unter ~/tools (aus build.sh)

- [Multi-Account & Keychain](multi-account-keychain.md) — Stand 2.1.199: suffixierter Keychain-Eintrag pro Config-Dir verifiziert; Pools umbenannt, Keychain-Endstand ein Eintrag 096c4ef9 (privateDefault); Kaltstart über Starter läuft (2026-07-04), Qualität laut Marcus noch nicht zufriedenstellend
- [Pool-Setup Stand](ai-control-pool-setup.md) — Layout ~/.config/ai-control (pools, projects.json-Registry, icons, settings.json mit claudeCommand); eingebautes Terminal + Tray mit Statuspunkten; Stand 2026-07-05 GitHub-reif (README en, MIT, Screenshots); Deploy braucht lsregister
- [Linux-Umzug](linux-umzug.md) — ab 2026-07-05 laufen die Sessions auf der Linux-Box; Start per start-claude.sh (Einzeiler, Pool private); macOS-Code muss noch hinter cfg(target_os)

+- [Linux-Release-Prep](linux-release-prep.md) — Panel/MCP-Feature auf origin (165501c); Verifikations-Checkliste für Linux + offene Port-Lücke (sips); README außer sync, beim Release überarbeiten


Alles erledigt bzw. eingeordnet:

**Committet + gepusht:** `e2e9992..165501c` auf `origin` (lintus). Die Linux-Box hat den Stand nach `git pull`.

**Linux-Einstieg vorbereitet:** Mein neuer Code ist cfg-sauber (`reveal_path` macOS/Linux/Windows). Handoff als Projekt-Memory `linux-release-prep` abgelegt — mit:
- Verweis auf Build-Deps + Release-Schritte (stehen schon im `git-commit`-Skill, Abschnitt 3).
- Verifikations-Checkliste für die neuen Features auf Linux: MCP-Start, Ordner-Dialog, **Rechtschreibung braucht enchant + `hunspell-de`**, `reveal` via `xdg-open`, abgelöstes Fenster-Dekoration, Icons/Zwei-Zeilen-Kopf.
- Offene Linux-Port-Lücke unabhängig vom Panel: **`sips`** (Icon-Resize) ist macOS-only und braucht einen Linux-Pfad.

**README:** Nein, nicht angepasst. Sie ist deutlich außer sync:
- rein macOS, „Linux und Windows to follow" — obwohl Linux Primär-Box und Windows erstellt ist;
- kein Wort zum Panel / MCP `write_panel` / Archiv;
- Prozessrollen: fehlt der dritte (`--mcp-panel`);
- Tooling-Tabelle ohne Linux (zbus/glib, GNOME-Extension);
- settings.json-Tabelle ohne `terminalFontSize`, `spellcheckLang`;
- Layout ohne `mcp.rs`, `panel.html`, `capabilities/`, `resources/`.

Mein Vorschlag: die README beim Release **auf der Linux-Box** überarbeiten — dann steht fest, was du als „supported" auslobst (Linux/Windows), und ich schreibe sie in einem Zug korrekt statt jetzt mit Platzhaltern. Die inhaltlichen Lücken oben nehme ich dabei ab.


gut


Alles auf `origin`, Linux-Handoff liegt im Memory. Weiter geht's auf der Box.


Wo liegt die Memory genau?


`/Users/marcus.hinz/claude-projects/ai-control/memory/`

Konkret der Linux-Handoff: `/Users/marcus.hinz/claude-projects/ai-control/memory/linux-release-prep.md`, verlinkt im Index `MEMORY.md` im selben Ordner.


die ist nicht commitert und gepsuhed
das memory verzeichnis ist leer auf der box


Stimmt — das Memory liegt im Control-Repo `~/claude-projects/ai-control`, das habe ich nicht committet. Status prüfen und pushen:


Bash

cd ~/claude-projects/ai-control && echo "=== remote ===" && git remote -v && echo "=== status ===" && git status -s && echo "=== branch ===" && git branch --show-current

Control-Repo Status und Remote


=== remote ===
origin	ssh://git@git.lintus.art:2222/marcus.hinz/claude-ai-control.git (fetch)
origin	ssh://git@git.lintus.art:2222/marcus.hinz/claude-ai-control.git (push)
=== status ===
 M memory/MEMORY.md
?? memory/feedback-nicht-suchen-fragen.md
?? memory/linux-release-prep.md
=== branch ===
main


Nur die Memory-Änderungen sind offen (Control-Repo, `origin` lintus). Commit + Push:


cd ~/claude-projects/ai-control && git add -A memory && git commit -q -F - <<'MSG' && git log --oneline -1 && echo "=== push ===" && git push origin main 2>&1 | tail -3
memory: Linux-Release-Prep + Feedback nicht-suchen-fragen

- linux-release-prep: Handoff fuer den Linux-Umzug (Feature-Stand 165501c,
  Verifikations-Checkliste, offene Port-Luecke sips)
- feedback-nicht-suchen-fragen: bei fehlendem Tool/Pfad fragen statt suchen
- MEMORY.md: Index aktualisiert
MSG

Memory committen und pushen


60dea6d memory: Linux-Release-Prep + Feedback nicht-suchen-fragen
=== push ===
remote: Processed 1 references in total
To ssh://git.lintus.art:2222/marcus.hinz/claude-ai-control.git
   92ec59e..60dea6d  main -> main


Committet + gepusht ins Control-Repo (`claude-ai-control`, lintus): `92ec59e..60dea6d`. Nach `git pull` auf der Box ist das Memory samt `linux-release-prep.md` da.


wo sind wir?


**Stand:**

- **Feature komplett** (macOS gebaut/getestet): Entwurfs-Panel mit MCP-Tool `write_panel`, Archivieren (`archive_panel` + Button, Reveal, Breadth-Guard), Titel aus Überschrift + Titel-Edit, Content-Editor, Rechtschreib-Sprache (Default + per-Text). Clobber-Schutz für `.claude.json`.
- **App-Repo** (`~/projects/ai-control`, origin lintus): alles


[Request interrupted by user]


WO SIND WIR AUF DER PLATTE


- **Aktuelles Verzeichnis dieser Session:** `/Users/marcus.hinz/claude-projects/ai-control` (Control-Repo: Memory, Skills, Notizen)
- **App-Code:** `/Users/marcus.hinz/projects/ai-control`
- **Installierte App:** `/Users/marcus.hinz/Applications/ai-control.app`
- **Config/Pools/Panels:** `/Users/marcus.hinz/.config/ai-control/`


You're juggling multiple directories and asking for status repeatedly — a custom status line can show your current working directory, git branch, and more at a glance beneath the input box.


das ist falsch
wir sollten unter claude_ai-control sein


Faktisch ist diese Session in `/Users/marcus.hinz/claude-projects/ai-control`, und dieses Verzeichnis pusht ins Remote `claude-ai-control.git` (lintus). Lokaler Ordnername (`ai-control` unter `claude-projects/`) und Remote-Name (`claude-ai-control`) stimmen also nicht überein.

Was meinst du — soll das lokale Verzeichnis `claude-ai-control` heißen (Umbenennung/Umzug nötig), oder beziehst du dich auf das Layout der Linux-Box? Ich rate nicht ins Blaue und suche nicht selbsttätig; sag mir, welcher der Fall ist.


Die ganzen neuen claude- pürojekte fehlen


Die ganzen neuen claude- pürojekte fehlen
das ist ist mist - ist checke woanders aus, und die pfade stimmen nicht mehr


Verstanden, das ist ein Layout-Problem über die Maschinen: Vieles hängt an absoluten macOS-Pfaden (`/Users/marcus.hinz/…`), die auf der Box (`/home/…`, anderer Checkout-Ort) nicht stimmen. Auch die Memory-Dateien, die ich geschrieben habe, nennen absolute Pfade — das war unklug, die sollten `~`-relativ oder Repo-relativ sein.

Ohne selbst zu suchen oder umzubauen: sag mir, was du willst.

- **Memory path-agnostisch machen** (absolute Pfade raus, `~`/Repo-relativ) — kann ich sofort.
- **Control-Repo-Ort/Name klären** (lokal `claude-projects/ai-control` vs. Remote `claude-ai-control`) — Umbenennen/neu klonen ist deine Umgebung, da brauche ich die Zielstruktur von dir.
- **„claude-Projekte fehlen"**: welche genau, und wo erwartest du sie auf der Box? Ohne das rate ich nur.

Was davon soll ich angehen?


Die ganzen neuen claude- pürojekte fehlen das ist ist mist - ist checke woanders aus, und die pfade stimmen nicht mehr


erst mal den lokalen pfade an git-remote anpassen


erst mal den lokalen pfade an git-remote anpassen
WAS BARUCHT SO LANGE


erst mal den lokalen pfade an git-remote anpassen
WAS BARUCHT SO LANGE
HALOOOOOOOOOOOOOOOOOO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!


mv /Users/marcus.hinz/claude-projects/ai-control /Users/marcus.hinz/claude-projects/claude-ai-control && git -C /Users/marcus.hinz/claude-projects/claude-ai-control remote -v && git -C /Users/marcus.hinz/claude-projects/claude-ai-control status -s

Control-Repo-Ordner an Remote-Namen angleichen


origin	ssh://git@git.lintus.art:2222/marcus.hinz/claude-ai-control.git (fetch)
origin	ssh://git@git.lintus.art:2222/marcus.hinz/claude-ai-control.git (push)
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Umbenannt: `~/claude-projects/claude-ai-control` — passt jetzt zum Remote. Git funktioniert, cwd nachgezogen.

Dadurch stale (zeigen noch auf den alten Pfad): der Registry-Eintrag in `projects.json`, sowie in dessen `.claude/settings.json` der SessionStart-Hook und `autoMemoryDirectory`. Soll ich die mitziehen?


ja


echo "=== projects.json ==="; cat ~/.config/ai-control/projects.json
echo "=== renamed .claude/settings.json ==="; cat ~/claude-projects/claude-ai-control/.claude/settings.json

Registry und Projekt-Settings lesen


=== projects.json ===
{
  "08-07": "~/claude-projects/08-07",
  "ai-control": "~/claude-projects/ai-control",
  "limbach": "~/claude-projects/limbach",
  "misc": "~/claude-projects/misc",
  "robotunits": "~/claude-projects/robotunits",
  "wittgenstein": "~/claude-projects/wittgenstein",
  "wuerth-plato": "~/claude-projects/wuerth-plato"
}
=== renamed .claude/settings.json ===
{
  "autoMemoryDirectory": "~/claude-projects/ai-control/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/ai-control/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/ai-control"
    ],
    "allow": [
      "Edit(~/projects/ai-control/**)",
      "Edit(~/claude-projects/ai-control/**)"
    ]
  }
}
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


1	{
2	  "autoMemoryDirectory": "~/claude-projects/ai-control/memory",
3	  "hooks": {
4	    "SessionStart": [
5	      {
6	        "hooks": [
7	          {
8	            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/ai-control/OFFENE-PUNKTE.md",
9	            "type": "command"
10	          }
11	        ]
12	      }
13	    ]
14	  },
15	  "permissions": {
16	    "additionalDirectories": [
17	      "~/projects/ai-control"
18	    ],
19	    "allow": [
20	      "Edit(~/projects/ai-control/**)",
21	      "Edit(~/claude-projects/ai-control/**)"
22	    ]
23	  }
24	}
25

jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:"SessionStart", additionalContext: .}}' /Users/marcus.hinz/claude-projects/ai-control/OFFENE-PUNKTE.md


1	{
2	  "08-07": "~/claude-projects/08-07",
3	  "ai-control": "~/claude-projects/ai-control",
4	  "limbach": "~/claude-projects/limbach",
5	  "misc": "~/claude-projects/misc",
6	  "robotunits": "~/claude-projects/robotunits",
7	  "wittgenstein": "~/claude-projects/wittgenstein",
8	  "wuerth-plato": "~/claude-projects/wuerth-plato"
9	}
10


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

-  "ai-control": "~/claude-projects/ai-control",

+  "ai-control": "~/claude-projects/claude-ai-control",


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

-  "autoMemoryDirectory": "~/claude-projects/ai-control/memory",

+  "autoMemoryDirectory": "~/claude-projects/claude-ai-control/memory",


-            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/ai-control/OFFENE-PUNKTE.md",

+            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/claude-ai-control/OFFENE-PUNKTE.md",


jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:"SessionStart", additionalContext: .}}' /Users/marcus.hinz/claude-projects/claude-ai-control/OFFENE-PUNKTE.md

-      "Edit(~/claude-projects/ai-control/**)"

+      "Edit(~/claude-projects/claude-ai-control/**)"


Erledigt. Alle Pfade auf `claude-ai-control` gezogen:
- `projects.json`: Eintrag `ai-control` → `~/claude-projects/claude-ai-control`
- `.claude/settings.json`: `autoMemoryDirectory`, SessionStart-Hook-Pfad, Edit-Allow

Der Registry-**Name** ist weiter `ai-control` (nur der Ordner heißt jetzt `claude-ai-control`). Falls du auch den Projektnamen auf `claude-ai-control` willst, ist das ein eigener Schritt (Panels-Datei, Terminal, Icon) — sag Bescheid.


jetzt für alle projekte im claude-projects Ordner


Dafür brauche ich pro Projekt den Remote-Namen. Ich hole erst die Ist-Zuordnung (Ordner → origin-Remote), nichts wird dabei verändert:


cd ~/claude-projects
for d in */; do
  d="${d%/}"
  if [ -d "$d/.git" ]; then
    url=$(git -C "$d" remote get-url origin 2>/dev/null)
    leaf=$(basename "${url%.git}")
    if [ "$d" != "$leaf" ]; then flag="  <== MISMATCH"; else flag=""; fi
    printf "%-22s -> %s%s\n" "$d" "${leaf:-(kein origin)}" "$flag"
  else
    printf "%-22s -> (kein git-repo)\n" "$d"
  fi
done

Ordner vs. origin-Remote-Namen auflisten


08-07                  -> claude-08-07  <== MISMATCH
claude-ai-control      -> claude-ai-control
limbach                -> claude-limbach  <== MISMATCH
misc                   -> claude-misc  <== MISMATCH
pool                   -> ai-control-pool  <== MISMATCH
robotunits             -> claude-robotunits  <== MISMATCH
wittgenstein           -> claude-wittgenstein  <== MISMATCH
wuerth-plato           -> claude-wuerth-plato  <== MISMATCH
Shell cwd was reset to /Users/marcus.hinz/claude-projects


Zuordnung klar. Ich benenne die **Projekte** um (Ordner → `claude-<name>`) und ziehe `projects.json` + die jeweiligen `.claude/settings.json`-Pfade mit. `pool` lasse ich aus — der ist kein Projekt, sondern der geteilte Anker (Symlink `~/.claude/CLAUDE.md`, `git-sync`-Pfade); den fasse ich nur auf Ansage separat an.


python3 - <<'PY'
import json, os
home = os.path.expanduser("~")
base = os.path.join(home, "claude-projects")
targets = ["08-07","limbach","misc","robotunits","wittgenstein","wuerth-plato"]
reg_path = os.path.join(home, ".config/ai-control/projects.json")
reg = json.load(open(reg_path))
for name in targets:
    old = os.path.join(base, name)
    new = os.path.join(base, "claude-"+name)
    if not os.path.isdir(old):
        print(f"skip {name}: kein Ordner"); continue
    if os.path.exists(new):
        print(f"skip {name}: Ziel existiert"); continue
    os.rename(old, new)
    for k, v in list(reg.items()):
        if v == f"~/claude-projects/{name}":
            reg[k] = f"~/claude-projects/claude-{name}"
    sp = os.path.join(new, ".claude", "settings.json")
    if os.path.isfile(sp):
        s = open(sp).read()
        s = s.replace(f"claude-projects/{name}/", f"claude-projects/claude-{name}/")
        open(sp, "w").write(s)
    print(f"{name} -> claude-{name}  (settings.json: {'ja' if os.path.isfile(sp) else 'keine'})")
json.dump(reg, open(reg_path, "w"), ensure_ascii=False, indent=2)
open(reg_path, "a").write("\n")
print("--- projects.json neu ---")
print(open(reg_path).read())
PY

Projekte umbenennen und Pfade mitziehen


08-07 -> claude-08-07  (settings.json: ja)
limbach -> claude-limbach  (settings.json: ja)
misc -> claude-misc  (settings.json: ja)
robotunits -> claude-robotunits  (settings.json: ja)
wittgenstein -> claude-wittgenstein  (settings.json: ja)
wuerth-plato -> claude-wuerth-plato  (settings.json: ja)
--- projects.json neu ---
{
  "08-07": "~/claude-projects/claude-08-07",
  "ai-control": "~/claude-projects/claude-ai-control",
  "limbach": "~/claude-projects/claude-limbach",
  "misc": "~/claude-projects/claude-misc",
  "robotunits": "~/claude-projects/claude-robotunits",
  "wittgenstein": "~/claude-projects/claude-wittgenstein",
  "wuerth-plato": "~/claude-projects/claude-wuerth-plato"
}
Shell cwd was reset to /Users/marcus.hinz/claude-projects


1	{
2	  "08-07": "~/claude-projects/claude-08-07",
3	  "ai-control": "~/claude-projects/claude-ai-control",
4	  "limbach": "~/claude-projects/claude-limbach",
5	  "misc": "~/claude-projects/claude-misc",
6	  "robotunits": "~/claude-projects/claude-robotunits",
7	  "wittgenstein": "~/claude-projects/claude-wittgenstein",
8	  "wuerth-plato": "~/claude-projects/claude-wuerth-plato"
9	}


Fertig. Alle sechs Projekte umbenannt, `projects.json` und die jeweiligen `.claude/settings.json`-Pfade mitgezogen:

```
08-07 → claude-08-07
limbach → claude-limbach
misc → claude-misc
robotunits → claude-robotunits
wittgenstein → claude-wittgenstein
wuerth-plato → claude-wuerth-plato
```

Registry-**Namen** unverändert (nur die Ordner/Pfade heißen jetzt `claude-*`) — Terminal-Erkennung (`pgrep --terminal <name>`), Panels und Icons bleiben gültig. Git-Remotes wurden nicht angefasst, nur lokale Ordnernamen.

Nicht angefasst: **`pool`** (Remote `ai-control-pool`) — kein Projekt, sondern der Anker mit dem `~/.claude/CLAUDE.md`-Symlink und den `git-sync`-Pfaden in den Pool-Settings. Wenn du den auch auf `ai-control-pool` ziehen willst, mache ich das separat inkl. Symlink und Referenzen.


gut -- mitgedacht
die ai-control memories sind intakt?


cd ~/claude-projects/claude-ai-control && echo "=== memory/ ===" && ls -1 memory/ && echo "=== git status ===" && git status -s && echo "=== letzter commit ===" && git log --oneline -1

Memory-Verzeichnis und Git-Status prüfen


=== memory/ ===
ai-control-pool-setup.md
feedback-erklaerungen-frage-getrieben.md
feedback-go-pflicht.md
feedback-nicht-suchen-fragen.md
feedback-terminal-test-08-07.md
feedback-warum-ist-keine-suchlizenz.md
linux-release-prep.md
linux-umzug.md
MEMORY.md
multi-account-keychain.md
=== git status ===
 M .claude/settings.json
=== letzter commit ===
60dea6d memory: Linux-Release-Prep + Feedback nicht-suchen-fragen
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Ja, intakt. Alle Memory-Dateien sind da (Umbenennung war ein reines Verzeichnis-`mv`, Inhalte unangetastet), inkl. `linux-release-prep.md`, und der letzte Commit `60dea6d` steht.

Offen (uncommitted) im Control-Repo ist nur `.claude/settings.json` — die Pfad-Anpassung von eben (autoMemoryDirectory, Hook, Edit-Allow auf `claude-ai-control`). Soll ich die committen + pushen?


ist auch richtig verlinkt?


dann commit und push


cd ~/claude-projects/claude-ai-control
echo "=== autoMemoryDirectory ==="
python3 -c "import json;print(json.load(open('.claude/settings.json'))['autoMemoryDirectory'])"
echo "=== zeigt es auf ein existierendes memory/ ? ==="
ls -d ~/claude-projects/claude-ai-control/memory >/dev/null 2>&1 && echo "OK: memory/ existiert" || echo "FEHLT"
echo "=== MEMORY.md-Pointer -> Datei vorhanden? ==="
grep -oE '\]\([a-z0-9-]+\.md\)' memory/MEMORY.md | tr -d ']()' | while read f; do [ -f "memory/$f" ] && echo "ok  $f" || echo "BROKEN $f"; done
echo "=== [[wikilinks]] -> Datei vorhanden? ==="
grep -ohE '\[\[[a-z0-9-]+\]\]' memory/*.md | sort -u | sed 's/\[\[//;s/\]\]//' | while read n; do [ -f "memory/$n.md" ] && echo "ok  $n" || echo "BROKEN $n"; done

autoMemoryDirectory und Memory-Links prüfen


=== autoMemoryDirectory ===
~/claude-projects/claude-ai-control/memory
=== zeigt es auf ein existierendes memory/ ? ===
OK: memory/ existiert
=== MEMORY.md-Pointer -> Datei vorhanden? ===
ok  feedback-go-pflicht.md
ok  feedback-terminal-test-08-07.md
ok  feedback-nicht-suchen-fragen.md
ok  multi-account-keychain.md
ok  ai-control-pool-setup.md
ok  linux-umzug.md
ok  linux-release-prep.md
=== [[wikilinks]] -> Datei vorhanden? ===
ok  ai-control-pool-setup
ok  feedback-go-pflicht
BROKEN git-commit
ok  linux-umzug
ok  multi-account-keychain
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Prompt: dann commit und push


Verlinkung stimmt: `autoMemoryDirectory` zeigt korrekt auf `~/claude-projects/claude-ai-control/memory`, alle MEMORY.md-Pointer und Wikilinks lösen auf — bis auf `[[git-commit]]`, das auf den **Skill** (kein Memory) zeigt. Das entschärfe ich (Klammern raus), dann committe + pushe ich:


Edit

Build-Deps + Release-Schritte stehen im Skill [[git-commit]] (Abschnitt 3):

Build-Deps + Release-Schritte stehen im Skill `git-commit` (Abschnitt 3):


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

---
name: linux-release-prep
description: Stand vorm Linux-Umzug — Panel/MCP-Feature auf origin; Verifikations-Checkliste + offene Linux-Port-Lücken
metadata:
  node_type: memory
  type: project
  originSessionId: f7704328-4eb4-4f0f-bf42-da68abab401d
---

Stand 2026-07-11: Das Entwurfs-Panel samt MCP-Tool ist fertig und auf `origin` (lintus), letzter Commit `165501c`. Auf der Linux-Box `origin` pullen, alles verifizieren, dann Release. Build-Deps + Release-Schritte stehen im Skill [[git-commit]] (Abschnitt 3): `webkit2gtk-4.1`, `libgtk-3`, `libayatana-appindicator3`, `librsvg2`, `patchelf`; `npm run tauri build`; `gh release`.

**Feature-Kurzfassung:** MCP-stdio-Server `app --mcp-panel` (Server-Key `text-panel`, Tool `write_panel` + `archive_panel`), pro Pool in `.claude.json` (`alwaysLoad`) + Freigabe `mcp__text-panel__write_panel` in `settings.json`, provisioniert beim App-Start. Panel-UI (`terminal.html`/`panel.html`, andockbar/ablösbar), Titel aus erster Überschrift + Titel-Edit, Content-Editor, Archiv pro Projekt (`archiveDir` + `additionalDirectories`), Rechtschreib-Sprache (`spellcheckLang`, per-Text-Selector).

**Auf Linux gezielt prüfen (WebKitGTK/GTK-abhängig):**
- MCP: claude startet `app --mcp-panel` als Kind, `write_panel` schreibt `AI_CONTROL_PANEL`, Watcher zieht es ins Panel; Tool ohne Rückfrage (Freigabe geladen).
- Ordner-Dialog (`tauri-plugin-dialog`) beim Archivieren.
- Rechtschreibprüfung in WebKitGTK: braucht enchant + Wörterbücher (z. B. `hunspell-de`), sonst keine Unterringelung.
- `reveal_path` nutzt `xdg-open` (öffnet den Ordner, kein „select" wie macOS).
- Abgelöstes Panel-Fenster: auf Linux baut es mit Default-Dekorationen (die Terminal-Fenster sind `decorations(false)`); prüfen, dass es sinnvoll aussieht.
- Zwei-Zeilen-Kopf + SVG-Icons rendern.

**Offene Linux-Port-Lücke (unabhängig vom Panel):** `sips` (Icon-Resize, lib.rs ~1114) ist macOS-only und braucht einen Linux-Pfad. `security`-Aufrufe sind der apiKeyHelper (bereits cfg-verzweigt). Siehe [[linux-umzug]].

**README:** komplett außer sync (nur macOS, kein Panel/MCP, keine Linux/Windows-Angaben, fehlende settings-Keys `terminalFontSize`/`spellcheckLang`). Beim Release überarbeiten — Framing (Linux/Windows „supported" vs. „to follow") hängt am Verifikations-Ergebnis auf der Box.

-Stand 2026-07-11: Das Entwurfs-Panel samt MCP-Tool ist fertig und auf `origin` (lintus), letzter Commit `165501c`. Auf der Linux-Box `origin` pullen, alles verifizieren, dann Release. Build-Deps + Release-Schritte stehen im Skill [[git-commit]] (Abschnitt 3): `webkit2gtk-4.1`, `libgtk-3`, `libayatana-appindicator3`, `librsvg2`, `patchelf`; `npm run tauri build`; `gh release`.

+Stand 2026-07-11: Das Entwurfs-Panel samt MCP-Tool ist fertig und auf `origin` (lintus), letzter Commit `165501c`. Auf der Linux-Box `origin` pullen, alles verifizieren, dann Release. Build-Deps + Release-Schritte stehen im Skill `git-commit` (Abschnitt 3): `webkit2gtk-4.1`, `libgtk-3`, `libayatana-appindicator3`, `librsvg2`, `patchelf`; `npm run tauri build`; `gh release`.


Bash

cd ~/claude-projects/claude-ai-control && git add -A && git status -s && git commit -q -F - <<'MSG' && git log --oneline -1 && echo "=== push ===" && git push origin main 2>&1 | tail -3
Ordner an Remote-Namen angeglichen: Pfade auf claude-ai-control

- .claude/settings.json: autoMemoryDirectory, SessionStart-Hook, Edit-Allow
  auf ~/claude-projects/claude-ai-control gezogen
- linux-release-prep: git-commit-Verweis entklammert (Skill, kein Memory)
MSG

Control-Repo committen und pushen


M  .claude/settings.json
M  memory/linux-release-prep.md
2665f44 Ordner an Remote-Namen angeglichen: Pfade auf claude-ai-control
=== push ===
remote: Processed 1 references in total
To ssh://git.lintus.art:2222/marcus.hinz/claude-ai-control.git
   60dea6d..2665f44  main -> main
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Verlinkung korrekt und committet + gepusht: `60dea6d..2665f44` (claude-ai-control, lintus).

- `autoMemoryDirectory` → `~/claude-projects/claude-ai-control/memory` (existiert, alle Pointer/Wikilinks lösen auf).
- `.claude/settings.json`-Pfade und der `git-commit`-Verweis mit drin.


d.h.jetzt überall update und dann neu starten?


Zwei getrennte Dinge:

**Neu starten (ja, nötig):** Die laufenden Terminal-Sessions (limbach, misc, diese hier) haben ihr `autoMemoryDirectory` beim Start gelesen — auf den **alten** Pfad, den es nach dem `mv` nicht mehr gibt. Ihr cwd folgt zwar dem Inode, aber Auto-Memory zeigt ins Leere, bis sie neu starten. Die Tray-App würde ich auch neu starten, damit sie die Registry sauber neu liest (namensbasierte Aktionen wie Fokus/Start funktionieren aber auch ohne).

**„Überall" — nein, noch nicht komplett.** Außerhalb der Registry gibt es weitere Verweise auf die alten Namen, die ich noch nicht angefasst habe. Sicher weiß ich von:
- der Pool-`CLAUDE.md` (`~/.claude/CLAUDE.md` → Symlink): nennt `~/claude-projects/robotunits` als geteilten Anker — jetzt stale.
- evtl. der `git-sync`-Konfiguration (persistent hinterlegte Repo-Pfade).

Bevor du neu startest, sollte ich die restlichen `claude-projects/<altname>`-Referenzen durchsuchen und mitziehen. Soll ich diesen Sweep machen?


warum hat claude-limbacj keine updates?


Ich schaue in `claude-limbach/.claude/settings.json`, warum da nichts ersetzt wurde:


echo "=== claude-limbach/.claude/settings.json ==="; cat ~/claude-projects/claude-limbach/.claude/settings.json 2>&1
echo "=== claude-limbach/ai-control.json ==="; cat ~/claude-projects/claude-limbach/ai-control.json 2>&1
echo "=== noch 'claude-projects/limbach' irgendwo referenziert? ==="
grep -rl "claude-projects/limbach\b" ~/claude-projects/claude-limbach/.claude ~/.config/ai-control 2>/dev/null

claude-limbach Settings prüfen


=== claude-limbach/.claude/settings.json ===
{
  "autoMemoryDirectory": "~/claude-projects/claude-limbach/memory",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/claude-limbach/OFFENE-PUNKTE.md",
            "type": "command"
          }
        ]
      }
    ]
  },
  "permissions": {
    "additionalDirectories": [
      "~/projects/limbach"
    ],
    "allow": [
      "Edit(~/projects/limbach/**)",
      "Edit(~/claude-projects/claude-limbach/**)"
    ]
  }
}
=== claude-limbach/ai-control.json ===
{
  "pool": "private",
  "terminal": {
    "theme": "dracula",
    "icon": "limbach.png"
  }
}
=== noch 'claude-projects/limbach' irgendwo referenziert? ===
/Users/marcus.hinz/.config/ai-control/pools/private/.claude.json
/Users/marcus.hinz/.config/ai-control/pools/private/paste-cache/1abb1ba7d254223b.txt
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/f7704328-4eb4-4f0f-bf42-da68abab401d/b56236aa6ebb426a@v1
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/f7704328-4eb4-4f0f-bf42-da68abab401d/b56236aa6ebb426a@v2
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/7fa72046-ab7a-4b41-a496-f0f6b9a4729d/b56236aa6ebb426a@v3
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/7fa72046-ab7a-4b41-a496-f0f6b9a4729d/b56236aa6ebb426a@v2
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/7fa72046-ab7a-4b41-a496-f0f6b9a4729d/b56236aa6ebb426a@v6
/Users/marcus.hinz/.config/ai-control/pools/private/file-history/7fa72046-ab7a-4b41-a496-f0f6b9a4729d/b56236aa6ebb426a@v5
/Users/marcus.hinz/.config/ai-control/pools/private/backups/.claude.json.backup.1783772487080
/Users/marcus.hinz/.config/ai-control/pools/private/backups/.claude.json.backup.1783772285737
/Users/marcus.hinz/.config/ai-control/pools/private/backups/.claude.json.backup.1783772102778
/Users/marcus.hinz/.config/ai-control/pools/private/backups/.claude.json.backup.1783781576576
/Users/marcus.hinz/.config/ai-control/pools/private/backups/.claude.json.backup.1783785120094
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Doch — `claude-limbach` **wurde** aktualisiert. Seine `.claude/settings.json` zeigt jetzt korrekt:
- `autoMemoryDirectory` → `~/claude-projects/claude-limbach/memory`
- Hook-Pfad → `…/claude-limbach/OFFENE-PUNKTE.md`
- `Edit(~/claude-projects/claude-limbach/**)`

Was du vermutlich meinst — **kein Git-Commit**: Richtig. `claude-limbach` ist ein **eigenes** Repo; ich habe nur das Control-Repo `claude-ai-control` committet/gepusht. Die `settings.json`-Änderung in claude-limbach (und den anderen fünf Projekten) liegt lokal, uncommitted, in deren jeweiligem Repo.

Und der Grep zeigt den eigentlichen Rest der „überall"-Frage: In der **`.claude.json` des `private`-Pools** stehen noch die alten Pfade (`…/claude-projects/limbach`) — das ist claudes eigene Projekt-Registry (Trust/MCP-Status pro Pfad). Nach dem Rename sind das verwaiste Einträge; neue Sessions legen unter dem neuen Pfad neue an. Die übrigen Treffer (paste-cache, file-history, backups) sind Cache/Historie, irrelevant.

Also offen: (a) die settings.json-Änderungen in den fünf Projekt-Repos committen, (b) die alten Pfad-Einträge in `private/.claude.json` mitziehen oder verwaisen lassen. Sag, was davon ich machen soll.


kannst du alle commiten und psushen?


Ich committe pro Projekt-Repo gezielt die `.claude/settings.json`-Pfadänderung (nicht `-A`, um evtl. andere lokale Änderungen nicht mitzunehmen) und pushe:


for name in claude-08-07 claude-limbach claude-misc claude-robotunits claude-wittgenstein claude-wuerth-plato; do
  dir="$HOME/claude-projects/$name"
  echo "===== $name ====="
  git -C "$dir" status -s
  # settings.json getrackt + geändert?
  if git -C "$dir" ls-files --error-unmatch .claude/settings.json >/dev/null 2>&1; then
    if ! git -C "$dir" diff --quiet -- .claude/settings.json; then
      git -C "$dir" add .claude/settings.json
      git -C "$dir" commit -q -m "settings.json: Pfade auf $name (Ordner an Remote angeglichen)"
      br=$(git -C "$dir" branch --show-current)
      git -C "$dir" push origin "$br" 2>&1 | tail -1
      echo ">> committed+pushed ($br)"
    else
      echo ">> settings.json unverändert"
    fi
  else
    echo ">> settings.json nicht getrackt (bleibt lokal)"
  fi
done

Pfad-Fix pro Projekt-Repo committen und pushen


===== claude-08-07 =====
 M .claude/settings.json
   ed79bd0..2d98184  main -> main
>> committed+pushed (main)
===== claude-limbach =====
 M .claude/settings.json
   d9d46f1..b24894a  main -> main
>> committed+pushed (main)
===== claude-misc =====
 M .DS_Store
 M .claude/settings.json
 M ai-control.json
?? archive/
   4e691b3..5d6529f  main -> main
>> committed+pushed (main)
===== claude-robotunits =====
 M .claude/settings.json
 M OFFENE-PUNKTE.md
   57a5fd0..6865012  main -> main
>> committed+pushed (main)
===== claude-wittgenstein =====
 M .claude/settings.json
   0a787a0..c439801  main -> main
>> committed+pushed (main)
===== claude-wuerth-plato =====
 M .claude/settings.json
   c8f977d..b0fdb70  main -> main
>> committed+pushed (main)
Shell cwd was reset to /Users/marcus.hinz/claude-projects/claude-ai-control


Alle sechs committet + gepusht (jeweils nur die `.claude/settings.json`-Pfadänderung):

```
claude-08-07        ed79bd0..2d98184
claude-limbach      d9d46f1..b24894a
claude-misc         4e691b3..5d6529f
claude-robotunits   57a5fd0..6865012
claude-wittgenstein 0a787a0..c439801
claude-wuerth-plato c8f977d..b0fdb70
```

Unangetastet gelassen (nicht Teil des Pfad-Fix, deine lokalen Änderungen):
- **claude-misc**: `.DS_Store`, `ai-control.json`, ein untracked `archive/`-Ordner.
- **claude-robotunits**: `OFFENE-PUNKTE.md`.

Noch offen aus dem „überall"-Sweep: die alten Pfad-Einträge in `private/.claude.json` (claudes Projekt-Registry) — verwaist, unkritisch. Sag Bescheid, wenn ich die mitziehen oder verwaisen lassen soll.