Panel: Entwürfe archivieren, Titel aus Überschrift, Titel-Edit
- Archiv pro Projekt: Panel-Button „Archivieren" und MCP-Tool archive_panel legen den Entwurf als <YYYY-MM-DD_HHMM>-<slug>.md mit Frontmatter im Archiv-Ordner ab. Ordner in ai-control.json (archiveDir) + als additionalDirectories/Edit in .claude/settings.json (scanbar). Kein Ordner: Panel öffnet Ordner-Dialog, Zuruf nutzt Argument dir. - Panel-Titel zeigt die erste Überschrift des Entwurfs (Inline-Markdown gestrippt), Fallback „Entwurf"; Bleistift-Button macht ihn editierbar, schreibt die Überschrift via panel_set zurück. - Panel-Kopf zweizeilig: Titel + Edit oben, Aktionen rechtsbündig darunter. - AI_CONTROL_PROJECT als PTY-Env für den MCP-Server.
This commit is contained in:
@@ -7,10 +7,59 @@ export interface PanelView {
|
||||
|
||||
/// 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;
|
||||
@@ -42,10 +91,46 @@ export function initPanelView(opts: {
|
||||
}, 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,
|
||||
|
||||
@@ -14,12 +14,42 @@ 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
|
||||
|
||||
@@ -551,6 +551,9 @@ const view = initPanelView({
|
||||
content: panelContent,
|
||||
copyBtn: document.getElementById("panel-copy")!,
|
||||
modeBtn: document.getElementById("panel-mode")!,
|
||||
titleEl: panel.querySelector(".panel-title") as HTMLElement,
|
||||
editBtn: document.getElementById("panel-title-edit")!,
|
||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||
});
|
||||
|
||||
let detached = false;
|
||||
@@ -586,6 +589,34 @@ document
|
||||
.addEventListener("click", () => invoke("open_panel_window", { project }));
|
||||
document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Splitter: Panelbreite ziehen.
|
||||
splitter.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user