Archiv-Wiki im Panel: vier Tabs, FTS5-Suche, strukturierbares Archiv

- Archiv-Home pro Projekt (archiveHome in ai-control.json); archive_panel
  mit Ordner, Beschreibung und Schlagwörtern (Frontmatter + Zeitstempel-Stem)
- Archiv-Index (archive_index.rs): Scan mit Frontmatter, Wikilinks,
  Backlinks; Wiki-Seiten als strukturierte Daten (Übersicht/Tag-Seiten)
- FTS5-Suche (archive_search.rs, rusqlite bundled): in-memory-Index pro
  Anfrage; MCP search_archive und Panel-Suchfeld (search_run) teilen die Engine
- Panel: vier Puffer/Ansichten (Dokument, Befehle, Suche, Wiki) mit Tabs im
  Fenster-Header; gemeinsame Verdrahtung panel-wiring.ts, Kachel-CSS,
  Wiki-View mit Tag-Chips, Ordner-Sektionen und Backlinks
- Archivieren speichert offene Bearbeitung (flush); Tab-Wechsel speichert
  statt zu blockieren; Fehler sichtbar als Panel-Toast
- Abgelöstes Fenster: rahmenlos (Linux), eigene Kopfleiste mit Tabs und
  Fensterknöpfen, Theme-Kopplung über gemeinsames themes.ts
- MCP-Tools show_commands, show_archive; Befehls-History mit Kachel-Löschen
This commit is contained in:
marcus hinz
2026-07-19 15:25:40 +02:00
parent d4d455fa08
commit 994d5070f0
29 changed files with 3471 additions and 571 deletions
+58 -8
View File
@@ -1,8 +1,13 @@
import { marked } from "marked";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { flash } from "./commands-view";
export interface PanelView {
set(text: string): void;
raw(): string;
/// Schreibt eine offene Inhalts-Bearbeitung zurück (und beendet sie);
/// aufgelöst, sobald der Entwurf gespeichert ist.
flush(): Promise<void>;
}
/// Verkabelt einen Panel-Inhaltsbereich: MD/Roh-Umschalter und Copy-Button.
@@ -22,7 +27,7 @@ function stripInlineMd(s: string): string {
}
/// Titel für den Panel-Kopf: erste Überschrift (# …) oder sonst erste
/// nicht-leere Zeile, ohne Inline-Markdown; „Entwurf" bei leerem Text.
/// nicht-leere Zeile, ohne Inline-Markdown; „Dokument" bei leerem Text.
function firstLine(text: string): string {
let fallback = "";
for (const line of text.split("\n")) {
@@ -32,7 +37,7 @@ function firstLine(text: string): string {
if (t.startsWith("#") && h) return stripInlineMd(h);
if (!fallback) fallback = t;
}
return fallback ? stripInlineMd(fallback) : "Entwurf";
return fallback ? stripInlineMd(fallback) : "Dokument";
}
/// Ersetzt die erste Überschrift (bzw. legt eine an) im Rohtext durch `title`.
@@ -52,6 +57,45 @@ function setHeading(text: string, title: string): string {
return `# ${title}\n`;
}
/// Macht `[[name]]`-Wikilinks im gerendertem Markdown klickbar. Läuft über die
/// Textknoten des DOM statt über den Rohtext, damit Vorkommen in Code-Spans
/// und Code-Blöcken (z. B. bash `[[ -f x ]]`) unangetastet bleiben.
export function linkWikiRefs(root: HTMLElement, onClick: (name: string) => void) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes: Text[] = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
const t = n as Text;
if (!t.textContent?.includes("[[")) continue;
if (t.parentElement?.closest("code, pre, a")) continue;
nodes.push(t);
}
for (const node of nodes) {
const parts = node.textContent!.split(/\[\[([^\]]+)\]\]/g);
if (parts.length < 3) continue;
const frag = document.createDocumentFragment();
parts.forEach((part, i) => {
if (i % 2) {
// `[[ziel]]` oder `[[ziel|label]]`.
const sep = part.indexOf("|");
const target = sep < 0 ? part : part.slice(0, sep);
const label = sep < 0 ? part : part.slice(sep + 1);
const a = document.createElement("a");
a.href = "#";
a.className = "wiki";
a.textContent = label;
a.addEventListener("click", (e) => {
e.preventDefault();
onClick(target);
});
frag.append(a);
} else if (part) {
frag.append(part);
}
});
node.replaceWith(frag);
}
}
export function initPanelView(opts: {
content: HTMLElement;
copyBtn: HTMLElement;
@@ -64,7 +108,9 @@ export function initPanelView(opts: {
defaultLang?: string;
/// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
/// wurden.
onCommit?: (text: string) => void;
onCommit?: (text: string) => void | Promise<void>;
/// Klick auf einen `[[name]]`-Wikilink im gerenderten Markdown.
onWikiLink?: (name: string) => void;
}): PanelView {
let rawText = "";
let rendered = true;
@@ -72,11 +118,13 @@ export function initPanelView(opts: {
// gepuffert statt angewandt.
let editing = false;
let pending: string | null = null;
let flushEditor: () => Promise<void> = async () => {};
function draw() {
if (rendered) {
opts.content.className = "md";
opts.content.innerHTML = marked.parse(rawText, { async: false });
if (opts.onWikiLink) linkWikiRefs(opts.content, opts.onWikiLink);
} else {
opts.content.className = "raw";
opts.content.textContent = rawText;
@@ -90,9 +138,8 @@ export function initPanelView(opts: {
});
opts.copyBtn.addEventListener("click", async () => {
await navigator.clipboard.writeText(rawText);
opts.copyBtn.classList.add("copied");
setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
await writeText(rawText);
flash(opts.copyBtn, "copied");
});
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
@@ -172,11 +219,13 @@ export function initPanelView(opts: {
editBtn.classList.add("active");
editor.focus();
};
const save = () => {
flushEditor = async () => {
if (!editing) return;
const v = editor.value;
leave();
opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
await opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
};
const save = () => void flushEditor();
const cancel = () => {
const p = pending;
leave();
@@ -216,5 +265,6 @@ export function initPanelView(opts: {
applyText(text);
},
raw: () => rawText,
flush: () => flushEditor(),
};
}