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

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;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;

  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");
    const label = opts.copyBtn.textContent;
    opts.copyBtn.textContent = "Kopiert";
    setTimeout(() => {
      opts.copyBtn.classList.remove("copied");
      opts.copyBtn.textContent = label;
    }, 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);
  }

  draw();
  return {
    set(text: string) {
      rawText = text;
      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
        titleEl.textContent = firstLine(text);
      }
      draw();
    },
    raw: () => rawText,
  };
}

-    const label = opts.copyBtn.textContent;

-    opts.copyBtn.textContent = "Kopiert";

-      opts.copyBtn.classList.remove("copied");

-      opts.copyBtn.textContent = label;

+    setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);


ok -- das andocken braucht unbedingt ein icon - kopieren / archivieren, weenns geht auch und schließen sollte auch free float möglich sein


Add dockable text panel with copy functionality to terminal


`terminal.html` — SVG-Styling + Error-Klasse, und Buttons als Icons:


Edit

      .panel-btn.copied {
        color: #a6e3a1;
      }

      .panel-btn.copied {
        color: #a6e3a1;
      }
      .panel-btn.error {
        color: #f38ba8;
      }
      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }


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-btn.copied {
        color: #a6e3a1;
      }

      .panel-btn.copied {
        color: #a6e3a1;
      }
      .panel-btn.error {
        color: #f38ba8;
      }
      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }

.panel-btn.copied {
color: #a6e3a1;
}
+      .panel-btn.error {
+        color: #f38ba8;
+      }
+      .panel-btn svg {
+        display: block;
+        stroke: currentColor;
+        stroke-width: 1.3;
+        fill: none;
+        stroke-linecap: round;
+        stroke-linejoin: round;
+      }
#panel-content {
flex: 1;
min-height: 0;


            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
            <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
            <button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">⧉</button>
            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">✕</button>

            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</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-detach" title="In eigenes Fenster ablösen">
              <svg width="14" height="14" viewBox="0 0 16 16"><path d="M12.5 8.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h4.5" /><path d="M10 2h4v4" /><path d="M14 2 8 8" /></svg>
            </button>
            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">
              <svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
            </button>


            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
            <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
            <button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">⧉</button>
            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">✕</button>

            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</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-detach" title="In eigenes Fenster ablösen">
              <svg width="14" height="14" viewBox="0 0 16 16"><path d="M12.5 8.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h4.5" /><path d="M10 2h4v4" /><path d="M14 2 8 8" /></svg>
            </button>
            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">
              <svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
            </button>

-            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>

-            <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>

-            <button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">⧉</button>

-            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">✕</button>

+            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

+            <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">

+            <button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">

+            <button class="panel-btn" id="panel-hide" title="Panel ausblenden">


`panel.html` — SVG-Styling, Icons, und zusätzlich ein Schließen-Button neben Andocken:


      .panel-btn.copied {
        color: #a6e3a1;
      }

      .panel-btn.copied {
        color: #a6e3a1;
      }
      .panel-btn.error {
        color: #f38ba8;
      }
      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }


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-copy" title="In die Zwischenablage">Kopieren</button>
        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
        <button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button>


        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
        <button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button>

        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</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>


        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
        <button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button>

        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</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>

-        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>

-        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>

-        <button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button>

+        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

+        <button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">

+        <button class="panel-btn" id="panel-dock" title="Wieder andocken">

+        <button class="panel-btn" id="panel-close" title="Panel schließen">


Jetzt die JS-Logik. `terminal.ts` — Archiv-Flash auf Klasse und `panel-window-closed`-Listener:


// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
const archiveBtn = document.getElementById("panel-archive")!;
archiveBtn.addEventListener("click", async () => {
  const orig = archiveBtn.textContent;
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    archiveBtn.textContent = "Archiviert";
    archiveBtn.classList.add("copied");
  } catch (e) {
    archiveBtn.textContent = "Fehler";
    invoke("term_log", { msg: `archive: ${e}` });
  }
  setTimeout(() => {
    archiveBtn.textContent = orig;
    archiveBtn.classList.remove("copied");
  }, 1400);
});

// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }
});


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)

// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
const archiveBtn = document.getElementById("panel-archive")!;
archiveBtn.addEventListener("click", async () => {
  const orig = archiveBtn.textContent;
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    archiveBtn.textContent = "Archiviert";
    archiveBtn.classList.add("copied");
  } catch (e) {
    archiveBtn.textContent = "Fehler";
    invoke("term_log", { msg: `archive: ${e}` });
  }
  setTimeout(() => {
    archiveBtn.textContent = orig;
    archiveBtn.classList.remove("copied");
  }, 1400);
});

// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }
});

+function flashBtn(btn: HTMLElement, cls: string) {

+  setTimeout(() => btn.classList.remove(cls), 1400);

-    archiveBtn.textContent = "Archiviert";

-    archiveBtn.classList.add("copied");

-    archiveBtn.classList.remove("copied");


await listen("panel-attached", () => {
  detached = false;
  if (hasContent) showPanel();
});

await listen("panel-attached", () => {
  detached = false;
  if (hasContent) showPanel();
});
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
await listen("panel-window-closed", () => {
  detached = false;
});


await listen("panel-attached", () => {
  detached = false;
  if (hasContent) showPanel();
});
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
await listen("panel-window-closed", () => {
  detached = false;
});

+// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt

+// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.

+await listen("panel-window-closed", () => {


Rust: `Destroyed` des Panel-Fensters meldet jetzt `panel-window-closed`; das Andocken übernimmt das Frontend per `panel-attached`:


        if window.label().starts_with("panel-") {
          use tauri::{Emitter, Manager};
          let _ = window.app_handle().emit("panel-attached", ());

        if window.label().starts_with("panel-") {
          use tauri::{Emitter, Manager};
          let _ = window.app_handle().emit("panel-window-closed", ());


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)

        if window.label().starts_with("panel-") {
          use tauri::{Emitter, Manager};
          let _ = window.app_handle().emit("panel-attached", ());

        if window.label().starts_with("panel-") {
          use tauri::{Emitter, Manager};
          let _ = window.app_handle().emit("panel-window-closed", ());

if let tauri::WindowEvent::Destroyed = event {

-          let _ = window.app_handle().emit("panel-attached", ());

+          let _ = window.app_handle().emit("panel-window-closed", ());


`panel.ts` — Icons-Feedback, Andocken via `emit`, Schließen-Button:


import { listen } from "@tauri-apps/api/event";

import { listen, emit } from "@tauri-apps/api/event";


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 } 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")!,
  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")!;
archiveBtn.addEventListener("click", async () => {
  const orig = archiveBtn.textContent;
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    archiveBtn.textContent = "Archiviert";
    archiveBtn.classList.add("copied");
  } catch {
    archiveBtn.textContent = "Fehler";
  }
  setTimeout(() => {
    archiveBtn.textContent = orig;
    archiveBtn.classList.remove("copied");
  }, 1400);
});

// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das
// angedockte Panel im Terminal-Fenster erscheint wieder.
document
  .getElementById("panel-dock")!
  .addEventListener("click", () => win.close());

-import { listen } from "@tauri-apps/api/event";

+import { listen, emit } from "@tauri-apps/api/event";


// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
const archiveBtn = document.getElementById("panel-archive")!;
archiveBtn.addEventListener("click", async () => {
  const orig = archiveBtn.textContent;
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    archiveBtn.textContent = "Archiviert";
    archiveBtn.classList.add("copied");
  } catch {
    archiveBtn.textContent = "Fehler";
  }
  setTimeout(() => {
    archiveBtn.textContent = orig;
    archiveBtn.classList.remove("copied");
  }, 1400);
});

// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das
// angedockte Panel im Terminal-Fenster erscheint wieder.
document
  .getElementById("panel-dock")!
  .addEventListener("click", () => win.close());

// 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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } 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());


// 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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } 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());

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")!,
  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")!;
archiveBtn.addEventListener("click", async () => {
  const orig = archiveBtn.textContent;
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    archiveBtn.textContent = "Archiviert";
    archiveBtn.classList.add("copied");
  } catch {
    archiveBtn.textContent = "Fehler";
  }
  setTimeout(() => {
    archiveBtn.textContent = orig;
    archiveBtn.classList.remove("copied");
  }, 1400);
});

// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das
// angedockte Panel im Terminal-Fenster erscheint wieder.
document
  .getElementById("panel-dock")!
  .addEventListener("click", () => win.close());

+function flashBtn(btn: HTMLElement, cls: string) {

+  setTimeout(() => btn.classList.remove(cls), 1400);

-    archiveBtn.textContent = "Archiviert";

-    archiveBtn.classList.add("copied");

-    archiveBtn.classList.remove("copied");

-// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das

-// angedockte Panel im Terminal-Fenster erscheint wieder.

-  .addEventListener("click", () => win.close());

+// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.

+document.getElementById("panel-dock")!.addEventListener("click", async () => {

+// „Schließen": Fenster zu, ohne wieder anzudocken (Panel bleibt aus, bis ein

+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

Frontend + Rust bauen


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


Gebaut, Frontend + Rust grün.

**Icons:** Kopieren, Archivieren, Ablösen und Ausblenden (angedockt) sowie Kopieren, Archivieren, Andocken (abgelöst) sind jetzt SVG-Icons. `MD/Roh` bleibt Text (Zustands-Umschalter). Erfolg/Fehler zeigt sich als kurzer Farb-Flash (grün/rot) statt Textwechsel — sonst würde das Icon verschwinden.

**Frei schwebendes Fenster:** neben **Andocken** (Icon) jetzt ein **Schließen** (✕):
- **Andocken** → angedocktes Panel erscheint wieder, Fenster zu.
- **Schließen** (oder OS-Fensterschließen) → Fenster zu, Panel bleibt aus, bis ein neuer Entwurf kommt.

Dafür meldet das geschlossene Panel-Fenster jetzt `panel-window-closed` (statt automatisch anzudocken); das Andocken löst das Frontend gezielt per `panel-attached` aus.

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

Zum Ausprobieren: Dev neu starten (Rust-Änderung), frisches Terminal. Nichts committet.


erst mal so weit
was macht noch Sinn?


Aus dem, was in dieser Session aufgelaufen ist, in Reihenfolge:

1. **`.claude.json`-Clobber entschärfen** (der offene Punkt). Die Migration schreibt claudes Live-State-Datei bei jedem App-Start komplett neu — läuft parallel eine claude-Session, kann das ihren Stand überschreiben. Das ist ein echtes Korrektheits-/Datenrisiko, nicht Kosmetik. Fix: nur den `mcpServers.text-panel`-Schlüssel anfassen und nur schreiben, wenn er fehlt/abweicht. Das würde ich als Nächstes machen.

2. **Content-Editor im Panel** (das „größere Ding" von vorhin). Entwurf direkt editierbar, mit `panel-update`-Pause während der Bearbeitung und Rückschreiben. Logisch der nächste Ausbau nach dem Titel-Edit.

3. **Archiv-Feinschliff.** Kleiner, konkreter Nutzen: nach dem Archivieren „im Finder zeigen"-Button und der Archiv-Ordner als anklickbarer Pfad. Dazu: der Archiv-Ordner landet als `additionalDirectories` + Edit-Allow — wählt man dort etwas Breites (z. B. `~`), gibt das claude weiten Zugriff; ein Hinweis oder eine Begrenzung wäre sinnvoll.

4. **Linux/Windows-Check.** Panel, Ordner-Dialog und MCP sind plattformneutral gebaut, aber nur auf macOS gebaut/installiert. Vor dem nächsten Release einmal auf der Linux-Box verifizieren.

Mein Vorschlag: 1 zuerst (Risiko), dann 2 (der eigentliche Feature-Ausbau). 3 und 4 sind Politur bzw. Release-Vorbereitung.


Mach mal 1 - 3
dann ziehen wir aud linux um, machen da die Tests, dann das Relase auf github
dann Windows - ich muss das vor Realese auf GitHub aber noch testen

Wir müssen die READ.ME Überrabeiten, die ist inzwischen Komplet außer sync


Los mit 1–3. README ist notiert — die mache ich vor dem github-Release (nach den Linux-Tests), nicht jetzt.

**Item 1 — `.claude.json`-Clobber:** nur schreiben, wenn der Server-Eintrag fehlt/abweicht (sonst No-op, claudes Live-State bleibt unberührt):


Edit

fn register_mcp_server(pool_dir: &std::path::Path) {
  let Ok(exe) = std::env::current_exe() else {
    return;
  };
  let cfg = pool_dir.join(".claude.json");
  let mut v = fs::read_to_string(&cfg)
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .unwrap_or_else(|| serde_json::json!({}));
  let Some(obj) = v.as_object_mut() else { return };
  let Some(servers) = obj
    .entry("mcpServers")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
  else {
    return;
  };
  servers.remove("aicontrol"); // alter Key vor Umbenennung
  servers.insert(
    PANEL_MCP_SERVER.into(),
    serde_json::json!({
      "type": "stdio",
      "command": exe.to_string_lossy(),
      "args": ["--mcp-panel"],
      // Tool nicht deferren, sonst sieht das Modell write_panel nicht und
      // schreibt den Entwurf in den Chat statt ins Panel.
      "alwaysLoad": true,
    }),
  );
  if let Ok(out) = serde_json::to_string_pretty(&v) {
    let _ = fs::write(&cfg, out + "\n");
  }
}

fn register_mcp_server(pool_dir: &std::path::Path) {
  let Ok(exe) = std::env::current_exe() else {
    return;
  };
  // Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt
  // den Entwurf in den Chat statt ins Panel.
  let desired = serde_json::json!({
    "type": "stdio",
    "command": exe.to_string_lossy(),
    "args": ["--mcp-panel"],
    "alwaysLoad": true,
  });
  let cfg = pool_dir.join(".claude.json");
  let mut v = fs::read_to_string(&cfg)
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .unwrap_or_else(|| serde_json::json!({}));
  let Some(obj) = v.as_object_mut() else { return };
  let Some(servers) = obj
    .entry("mcpServers")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
  else {
    return;
  };
  // Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist
  // claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.
  if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {
    return;
  }
  servers.remove("aicontrol"); // alter Key vor Umbenennung
  servers.insert(PANEL_MCP_SERVER.into(), desired);
  if let Ok(out) = serde_json::to_string_pretty(&v) {
    let _ = fs::write(&cfg, out + "\n");
  }
}


fn register_mcp_server(pool_dir: &std::path::Path) {
  let Ok(exe) = std::env::current_exe() else {
    return;
  };
  let cfg = pool_dir.join(".claude.json");
  let mut v = fs::read_to_string(&cfg)
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .unwrap_or_else(|| serde_json::json!({}));
  let Some(obj) = v.as_object_mut() else { return };
  let Some(servers) = obj
    .entry("mcpServers")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
  else {
    return;
  };
  servers.remove("aicontrol"); // alter Key vor Umbenennung
  servers.insert(
    PANEL_MCP_SERVER.into(),
    serde_json::json!({
      "type": "stdio",
      "command": exe.to_string_lossy(),
      "args": ["--mcp-panel"],
      // Tool nicht deferren, sonst sieht das Modell write_panel nicht und
      // schreibt den Entwurf in den Chat statt ins Panel.
      "alwaysLoad": true,
    }),
  );
  if let Ok(out) = serde_json::to_string_pretty(&v) {
    let _ = fs::write(&cfg, out + "\n");
  }
}

fn register_mcp_server(pool_dir: &std::path::Path) {
  let Ok(exe) = std::env::current_exe() else {
    return;
  };
  // Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt
  // den Entwurf in den Chat statt ins Panel.
  let desired = serde_json::json!({
    "type": "stdio",
    "command": exe.to_string_lossy(),
    "args": ["--mcp-panel"],
    "alwaysLoad": true,
  });
  let cfg = pool_dir.join(".claude.json");
  let mut v = fs::read_to_string(&cfg)
    .ok()
    .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
    .unwrap_or_else(|| serde_json::json!({}));
  let Some(obj) = v.as_object_mut() else { return };
  let Some(servers) = obj
    .entry("mcpServers")
    .or_insert_with(|| serde_json::json!({}))
    .as_object_mut()
  else {
    return;
  };
  // Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist
  // claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.
  if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {
    return;
  }
  servers.remove("aicontrol"); // alter Key vor Umbenennung
  servers.insert(PANEL_MCP_SERVER.into(), desired);
  if let Ok(out) = serde_json::to_string_pretty(&v) {
    let _ = fs::write(&cfg, out + "\n");
  }
}

+  // Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt

+  // den Entwurf in den Chat statt ins Panel.

+  // Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist

+  // claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.

+  if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {

-      "command": exe.to_string_lossy(),

-      // Tool nicht deferren, sonst sieht das Modell write_panel nicht und

-      // schreibt den Entwurf in den Chat statt ins Panel.

+  servers.insert(PANEL_MCP_SERVER.into(), desired);


Mach mal 1 - 3  dann ziehen wir aud linux um, machen da die Tests, dann das Relase auf github dann Windows - ich muss das vor Realese auf GitHub aber noch testen  Wir müssen die READ.ME Überrabeiten,…


**Item 3 (Rust) — Breadth-Guard** im Archiv-Setzen und **Reveal-Command:**


pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;

pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {
    return Err(format!(
      "Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",
      expanded.display()
    ));
  }
  fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;


pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;

pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {
    return Err(format!(
      "Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",
      expanded.display()
    ));
  }
  fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;

+  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde

+  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.

+  if paths.home.starts_with(&expanded) {

+      "Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",

let contracted = contract_home(&paths, &expanded);

let mut cfg = read_project_config_in(&paths, project)?;


/// Archiviert den aktuellen Panel-Entwurf. `dir` (aus dem Ordner-Dialog) setzt
/// den Archiv-Ordner zugleich; ohne `dir` muss er konfiguriert sein.
#[tauri::command]
fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, String> {
  archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
}

/// Archiviert den aktuellen Panel-Entwurf. `dir` (aus dem Ordner-Dialog) setzt
/// den Archiv-Ordner zugleich; ohne `dir` muss er konfiguriert sein.
#[tauri::command]
fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, String> {
  archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
}

/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").
#[tauri::command]
fn reveal_path_cmd(path: String) {
  reveal_path(std::path::Path::new(&path));
}

#[cfg(target_os = "macos")]
fn reveal_path(path: &std::path::Path) {
  let _ = Command::new("open").arg("-R").arg(path).spawn();
}

#[cfg(target_os = "linux")]
fn reveal_path(path: &std::path::Path) {
  let target = path.parent().unwrap_or(path);
  let _ = Command::new("xdg-open").arg(target).spawn();
}

#[cfg(target_os = "windows")]
fn reveal_path(path: &std::path::Path) {
  let _ = Command::new("explorer")
    .arg(format!("/select,{}", path.display()))
    .spawn();
}


/// Archiviert den aktuellen Panel-Entwurf. `dir` (aus dem Ordner-Dialog) setzt
/// den Archiv-Ordner zugleich; ohne `dir` muss er konfiguriert sein.
#[tauri::command]
fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, String> {
  archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
}

/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").
#[tauri::command]
fn reveal_path_cmd(path: String) {
  reveal_path(std::path::Path::new(&path));
}

#[cfg(target_os = "macos")]
fn reveal_path(path: &std::path::Path) {
  let _ = Command::new("open").arg("-R").arg(path).spawn();
}

#[cfg(target_os = "linux")]
fn reveal_path(path: &std::path::Path) {
  let target = path.parent().unwrap_or(path);
  let _ = Command::new("xdg-open").arg(target).spawn();
}

#[cfg(target_os = "windows")]
fn reveal_path(path: &std::path::Path) {
  let _ = Command::new("explorer")
    .arg(format!("/select,{}", path.display()))
    .spawn();
}

+/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").

+fn reveal_path(path: &std::path::Path) {

+  let _ = Command::new("open").arg("-R").arg(path).spawn();

+  let target = path.parent().unwrap_or(path);

+  let _ = Command::new("xdg-open").arg(target).spawn();

+    .arg(format!("/select,{}", path.display()))

/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.

/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.


      panel_archive_dir_cmd,
      panel_archive_cmd,

      panel_archive_dir_cmd,
      panel_archive_cmd,
      reveal_path_cmd,


      panel_archive_dir_cmd,
      panel_archive_cmd,
      reveal_path_cmd,

// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.


**Item 3 (Frontend) — nach erfolgreichem Archivieren im Dateimanager zeigen.** `terminal.ts` und `panel.ts`:


    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }

    const path = await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
    invoke("reveal_path_cmd", { path });
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }


    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }

    const path = await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
    invoke("reveal_path_cmd", { path });
  } catch (e) {
    flashBtn(archiveBtn, "error");
    invoke("term_log", { msg: `archive: ${e}` });
  }

-    await invoke<string>("panel_archive_cmd", { project, dir });

+    const path = await invoke<string>("panel_archive_cmd", { project, dir });

+    invoke("reveal_path_cmd", { path });


    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } catch {
    flashBtn(archiveBtn, "error");
  }

    const path = await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
    invoke("reveal_path_cmd", { path });
  } catch {
    flashBtn(archiveBtn, "error");
  }


    const path = await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
    invoke("reveal_path_cmd", { path });
  } catch {
    flashBtn(archiveBtn, "error");
  }

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")!,
  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;
    }
    await invoke<string>("panel_archive_cmd", { project, dir });
    flashBtn(archiveBtn, "copied");
  } 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());


**Item 2 — Content-Editor.** Aktuellen `panel-view.ts`-Kern lesen, dann Editor einbauen:


38	/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
39	function setHeading(text: string, title: string): string {
40	  const lines = text.split("\n");
41	  for (let i = 0; i < lines.length; i++) {
42	    const t = lines[i].trim();
43	    if (!t) continue;
44	    if (t.startsWith("#")) {
45	      const hashes = (t.match(/^#+/) as RegExpMatchArray)[0];
46	      lines[i] = `${hashes} ${title}`;
47	    } else {
48	      lines.splice(i, 0, `# ${title}`, "");
49	    }
50	    return lines.join("\n");
51	  }
52	  return `# ${title}\n`;
53	}
54
55	export function initPanelView(opts: {
56	  content: HTMLElement;
57	  copyBtn: HTMLElement;
58	  modeBtn: HTMLElement;
59	  titleEl?: HTMLElement;
60	  editBtn?: HTMLElement;
61	  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
62	  onCommit?: (text: string) => void;
63	}): PanelView {
64	  let rawText = "";
65	  let rendered = true;
66
67	  function draw() {
68	    if (rendered) {
69	      opts.content.className = "md";
70	      opts.content.innerHTML = marked.parse(rawText, { async: false });
71	    } else {
72	      opts.content.className = "raw";
73	      opts.content.textContent = rawText;
74	    }
75	    opts.modeBtn.textContent = rendered ? "MD" : "Roh";
76	  }
77
78	  opts.modeBtn.addEventListener("click", () => {
79	    rendered = !rendered;
80	    draw();
81	  });
82
83	  opts.copyBtn.addEventListener("click", async () => {
84	    await navigator.clipboard.writeText(rawText);
85	    opts.copyBtn.classList.add("copied");
86	    setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
87	  });
88
89	  // Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
90	  // geänderte Überschrift zurück, Escape verwirft.
91	  const titleEl = opts.titleEl;
92	  if (opts.editBtn && titleEl && opts.onCommit) {
93	    const commit = () => {
94	      if (titleEl.getAttribute("contenteditable") !== "true") return;
95	      titleEl.removeAttribute("contenteditable");
96	      const nt = (titleEl.textContent || "").trim();
97	      if (nt && nt !== firstLine(rawText)) opts.onCommit!(setHeading(rawText, nt));
98	      else titleEl.textContent = firstLine(rawText);
99	    };
100	    opts.editBtn.addEventListener("click", () => {
101	      titleEl.setAttribute("contenteditable", "true");
102	      titleEl.focus();
103	      const r = document.createRange();
104	      r.selectNodeContents(titleEl);
105	      const sel = window.getSelection();
106	      sel?.removeAllRanges();
107	      sel?.addRange(r);
108	    });
109	    titleEl.addEventListener("keydown", (e) => {
110	      if (e.key === "Enter") {
111	        e.preventDefault();
112	        titleEl.blur();
113	      } else if (e.key === "Escape") {
114	        titleEl.textContent = firstLine(rawText);
115	        titleEl.removeAttribute("contenteditable");
116	        titleEl.blur();
117	      }
118	    });
119	    titleEl.addEventListener("blur", commit);
120	  }
121
122	  draw();
123	  return {
124	    set(text: string) {
125	      rawText = text;
126	      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
127	        titleEl.textContent = firstLine(text);
128	      }
129	      draw();
130	    },
131	    raw: () => rawText,
132	  };
133	}
134

/// 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;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;

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

  draw();
  return {
    set(text: string) {
      rawText = text;
      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
        titleEl.textContent = firstLine(text);
      }
      draw();
    },
    raw: () => rawText,
  };
}


  titleEl?: HTMLElement;
  editBtn?: HTMLElement;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;

  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;


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;
  /// 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;

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;
  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
  onCommit?: (text: string) => void;
}): PanelView {
  let rawText = "";
  let rendered = true;

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

  draw();
  return {
    set(text: string) {
      rawText = text;
      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
        titleEl.textContent = firstLine(text);
      }
      draw();
    },
    raw: () => rawText,
  };
}

-  /// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.

+  /// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert

+  // Content-Edit-Zustand: während des Editierens werden eingehende Updates


    titleEl.addEventListener("blur", commit);
  }

  draw();
  return {
    set(text: string) {
      rawText = text;
      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
        titleEl.textContent = firstLine(text);
      }
      draw();
    },
    raw: () => rawText,
  };
}

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


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

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

  draw();
  return {
    set(text: string) {
      rawText = text;
      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
        titleEl.textContent = firstLine(text);
      }
      draw();
    },
    raw: () => rawText,
  };
}

+  // 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");

+      editBtn.classList.remove("active", "changed");

+      opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert

+    editBtn.addEventListener("click", () => (editing ? save() : enter()));

+    editor.addEventListener("keydown", (e) => {

+      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {

+    if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {

+      titleEl.textContent = firstLine(text);

-      if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {

-        titleEl.textContent = firstLine(text);

+      // Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den

+      // Edit-Button als „geändert" markieren.

+        opts.editContentBtn?.classList.add("changed");


CSS und Button in `terminal.html`:


      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }

      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }
      .panel-btn.active {
        background: #313244;
        color: #cdd6f4;
      }
      .panel-btn.changed {
        color: #f9e2af;
      }
      .panel-editor {
        flex: 1;
        min-height: 0;
        resize: none;
        border: none;
        outline: none;
        padding: 14px 16px;
        background: transparent;
        color: inherit;
        font-family: "JetBrains Mono", Menlo, monospace;
        font-size: 12px;
        line-height: 1.55;
      }
      .panel-editor[hidden] {
        display: none;
      }


      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }
      .panel-btn.active {
        background: #313244;
        color: #cdd6f4;
      }
      .panel-btn.changed {
        color: #f9e2af;
      }
      .panel-editor {
        flex: 1;
        min-height: 0;
        resize: none;
        border: none;
        outline: none;
        padding: 14px 16px;
        background: transparent;
        color: inherit;
        font-family: "JetBrains Mono", Menlo, monospace;
        font-size: 12px;
        line-height: 1.55;
      }
      .panel-editor[hidden] {
        display: none;
      }

+        font-family: "JetBrains Mono", Menlo, monospace;


            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

            <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">


            <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
            <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

            <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">

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


Dasselbe CSS + Button in `panel.html`:


      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }

      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }
      .panel-btn.active {
        background: #313244;
        color: #cdd6f4;
      }
      .panel-btn.changed {
        color: #f9e2af;
      }
      .panel-editor {
        flex: 1;
        min-height: 0;
        resize: none;
        border: none;
        outline: none;
        padding: 16px 20px;
        background: transparent;
        color: inherit;
        font-family: "JetBrains Mono", Menlo, monospace;
        font-size: 12px;
        line-height: 1.55;
      }
      .panel-editor[hidden] {
        display: none;
      }


      .panel-btn svg {
        display: block;
        stroke: currentColor;
        stroke-width: 1.3;
        fill: none;
        stroke-linecap: round;
        stroke-linejoin: round;
      }
      .panel-btn.active {
        background: #313244;
        color: #cdd6f4;
      }
      .panel-btn.changed {
        color: #f9e2af;
      }
      .panel-editor {
        flex: 1;
        min-height: 0;
        resize: none;
        border: none;
        outline: none;
        padding: 16px 20px;
        background: transparent;
        color: inherit;
        font-family: "JetBrains Mono", Menlo, monospace;
        font-size: 12px;
        line-height: 1.55;
      }
      .panel-editor[hidden] {
        display: none;
      }

<!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-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>


        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

        <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">


        <button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
        <button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">

        <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">

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


Verkabelung `editContentBtn` in `terminal.ts` und `panel.ts`:


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

  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")!,
  onCommit: (text) => invoke("panel_set", { project, text }),
});

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

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

+  editContentBtn: document.getElementById("panel-content-edit")!,


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

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


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")!,
  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

Frontend + Rust bauen (Items 1-3)


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


Beide grün. Tests:


export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup"; export PATH="$CARGO_HOME/bin:$PATH"; cargo test 2>&1 | grep -E "test result: ok. 5|FAILED|error" | head

cargo test


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


55 grün. Kurzer Check des Breadth-Guards (Item 3) headless — `dir="~"` muss abgelehnt werden, ohne die Config zu ändern:


SCRATCH="/private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/f7704328-4eb4-4f0f-bf42-da68abab401d/scratchpad"
BIN="/Users/marcus.hinz/projects/ai-control/src-tauri/target/debug/ai-control"
PANEL="$HOME/.config/ai-control/panels/misc.md"
MISCCFG="$HOME/claude-projects/misc/ai-control.json"
cp "$MISCCFG" "$SCRATCH/misc-cfg.bak"
printf '# Test\n\nInhalt\n' > "$PANEL"
echo "=== archive_panel dir=~ (Home, muss abgelehnt werden) ==="
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"archive_panel","arguments":{"dir":"~"}}}' \
| AI_CONTROL_PROJECT=misc AI_CONTROL_PANEL="$PANEL" "$BIN" --mcp-panel | python3 -c "import json,sys
for l in sys.stdin:
    d=json.loads(l)
    if d.get('id')==2:
        r=d['result']; print('isError:', r.get('isError')); print('text:', r['content'][0]['text'][:120])"
echo "=== misc/ai-control.json unverändert (kein archiveDir)? ==="
python3 -c "import json;print('archiveDir:', json.load(open('$MISCCFG')).get('archiveDir'))"
cp "$SCRATCH/misc-cfg.bak" "$MISCCFG"; : > "$PANEL"; echo "restored"

Breadth-Guard: Home ablehnen, Config unverändert


=== archive_panel dir=~ (Home, muss abgelehnt werden) ===
isError: None
text: Archiviert: ~/2026-07-11_1205-test.md
=== misc/ai-control.json unverändert (kein archiveDir)? ===
archiveDir: ~
restored


Guard versagt: `expand_home` behandelt bare `~` nicht (nur `~/…`), also blieb `~` ein literaler relativer Pfad — Guard griff nicht, und es entstand sogar ein Ordner `~`. Zwei Fixe: `expand_home` für bare `~`, und absoluten Pfad erzwingen.


Edit

/// "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
  match p.strip_prefix("~/") {
    Some(rest) => paths.home.join(rest),
    None => PathBuf::from(p),
  }
}

/// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
  if p == "~" {
    return paths.home.clone();
  }
  match p.strip_prefix("~/") {
    Some(rest) => paths.home.join(rest),
    None => PathBuf::from(p),
  }
}


/// "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
  match p.strip_prefix("~/") {
    Some(rest) => paths.home.join(rest),
    None => PathBuf::from(p),
  }
}

/// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
  if p == "~" {
    return paths.home.clone();
  }
  match p.strip_prefix("~/") {
    Some(rest) => paths.home.join(rest),
    None => PathBuf::from(p),
  }
}

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

-/// "~/x" relativ zum Home auflösen; alles andere unverändert.

+/// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.


  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {

  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  if !expanded.is_absolute() {
    return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));
  }
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {


  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {

  let paths = Paths::real();
  let expanded = expand_home(&paths, dir);
  if !expanded.is_absolute() {
    return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));
  }
  // Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
  // claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
  if paths.home.starts_with(&expanded) {

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

+    return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));


Rebuild, den literalen `~`-Ordner aufräumen, und Guard erneut testen:


Bash

export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup"; export PATH="$CARGO_HOME/bin:$PATH"; cd ~/projects/ai-control/src-tauri && cargo build 2>&1 | grep -E "error|Finished" | head
echo "=== stray '~' Ordner suchen/aufräumen ==="
for d in "$HOME/claude-projects/ai-control/~" "$HOME/projects/ai-control/~" "$HOME/projects/ai-control/src-tauri/~"; do
  if [ -e "$d" ]; then echo "gefunden: $d"; ls -la "$d"; rm -rf "$d" && echo "entfernt"; fi
done
find "$HOME/claude-projects/ai-control" "$HOME/projects/ai-control" -maxdepth 2 -name '~' -type d 2>/dev/null

Build + stray ~-Ordner entfernen


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
=== stray '~' Ordner suchen/aufräumen ===
gefunden: /Users/marcus.hinz/projects/ai-control/src-tauri/~
total 8
drwxr-xr-x   3 marcus.hinz  staff   96 Jul 11 14:05 .
drwxr-xr-x  15 marcus.hinz  staff  480 Jul 11 14:05 ..
-rw-r--r--   1 marcus.hinz  staff  101 Jul 11 14:05 2026-07-11_1205-test.md
entfernt


Aufgeräumt (lag in `src-tauri/~`). Guard erneut testen mit dem neuen Binary:


SCRATCH="/private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/f7704328-4eb4-4f0f-bf42-da68abab401d/scratchpad"
BIN="/Users/marcus.hinz/projects/ai-control/src-tauri/target/debug/ai-control"
PANEL="$HOME/.config/ai-control/panels/misc.md"
MISCCFG="$HOME/claude-projects/misc/ai-control.json"
cp "$MISCCFG" "$SCRATCH/misc-cfg.bak"
printf '# Test\n\nInhalt\n' > "$PANEL"
run() {
  printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' \
  "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"archive_panel\",\"arguments\":{\"dir\":\"$1\"}}}" \
  | AI_CONTROL_PROJECT=misc AI_CONTROL_PANEL="$PANEL" "$BIN" --mcp-panel | python3 -c "import json,sys
for l in sys.stdin:
    d=json.loads(l)
    if d.get('id')==2: r=d['result']; print('  isError:',r.get('isError'),'|',r['content'][0]['text'][:90])"
}
cd "$SCRATCH"
echo "dir=~ (Home):"; run "~"
echo "dir=/ (Root):"; run "/"
echo "dir=relativ 'foo':"; run "foo"
echo "dir=$SCRATCH/ok-archiv (gültig):"; run "$SCRATCH/ok-archiv"
echo "=== ok-archiv Inhalt ==="; ls "$SCRATCH/ok-archiv" 2>&1
cp "$SCRATCH/misc-cfg.bak" "$MISCCFG"; : > "$PANEL"; rm -rf "$SCRATCH/ok-archiv"; echo restored

Guard: Home/Root/relativ ablehnen, gültigen Ordner akzeptieren


dir=~ (Home):
  isError: True | Nicht archiviert: Archiv-Ordner zu weit gefasst: /Users/marcus.hinz. Bitte einen spezifisc
dir=/ (Root):
  isError: True | Nicht archiviert: Archiv-Ordner zu weit gefasst: /. Bitte einen spezifischen Unterordner w
dir=relativ 'foo':
  isError: True | Nicht archiviert: Archiv-Ordner muss ein absoluter Pfad sein: foo. Zielordner im Argument
dir=/private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/f7704328-4eb4-4f0f-bf42-da68abab401d/scratchpad/ok-archiv (gültig):
  isError: None | Archiviert: /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-ai-control/f7704328
=== ok-archiv Inhalt ===
2026-07-11_1207-test.md
restored
Shell cwd was reset to /Users/marcus.hinz/claude-projects/ai-control


Guard sauber: `~`, `/` und relative Pfade abgelehnt, gültiger absoluter Ordner archiviert; misc restored, keine Streu-Ordner. Tests:


export CARGO_HOME="$HOME/tools/.cargo" RUSTUP_HOME="$HOME/tools/.rustup"; export PATH="$CARGO_HOME/bin:$PATH"; cd ~/projects/ai-control/src-tauri && cargo test 2>&1 | grep -E "test result: ok. 5|FAILED|error" | head

cargo test


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


Items 1–3 fertig, gebaut, getestet (55 Rust-Tests grün, Frontend-Build grün, Guard headless verifiziert).

**1 — `.claude.json`-Clobber entschärft:** `register_mcp_server` schreibt nur noch, wenn der Eintrag fehlt oder abweicht (Alt-Key `aicontrol` oder anderer Pfad). Ist er schon korrekt → No-op, claudes Live-State bleibt unberührt.

**2 — Content-Editor:** Bleistift-Icon in der Button-Zeile schaltet den Inhalt auf eine Rohtext-Textarea. Cmd/Ctrl+Enter (oder Button erneut) speichert zurück via `panel_set`, Escape verwirft. Kommt während des Editierens ein neuer Entwurf (`panel-update`), wird er **gepuffert** statt zu überschreiben; der Button färbt sich gelb („geändert"). Speichern → deine Fassung gewinnt; Abbrechen → der gepufferte Stand wird übernommen. Angedockt und abgelöst.

**3 — Archiv-Feinschliff:**
- Nach dem Archivieren wird die Datei im Dateimanager gezeigt (`reveal_path_cmd`, cfg pro OS: macOS `open -R`, Linux `xdg-open`, Windows `explorer /select`).
- Breadth-Guard: der Archiv-Ordner darf kein Vorfahre von Home sein (Home/Root abgelehnt), muss absolut sein — sonst bekäme claude über `additionalDirectories` zu weiten Zugriff. Nebenbei `expand_home` für bare `~` gefixt.

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

Nichts committet. Für den Linux-Umzug (Box pullt von `origin`) muss das committet + gepusht werden.


ok reicht so erst mal, bis uaf eines - wenn's geht - Rechschreibprüfung?


Ja — im Content-Editor hatte ich `spellcheck` explizit ausgeschaltet. Ich schalte es an und gebe `de` als Sprachhinweis:


Edit

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

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