Entwurfs-Panel mit MCP-Tool write_panel

- Andockbares Panel im Terminal-Fenster: Markdown-Rendering, Copy-Button,
  normale Selektion; in eigenes Fenster ablösbar und wieder andockbar
  (Splitter, panel-detached/panel-attached).
- MCP-stdio-Server (app --mcp-panel) mit Tool write_panel; schreibt in die
  Datei aus AI_CONTROL_PANEL, Watcher zieht sie ins Panel.
- Provisionierung je Pool: Server-Registrierung in .claude.json (alwaysLoad),
  Freigabe mcp__text-panel__write_panel in settings.json, Panel-Skill; bei
  App-Start (alle Pools) und Pool-Anlage. Alte tee-Freigaben werden entfernt.
- Capability deckt panel-*-Fenster ab.
This commit is contained in:
marcus.hinz
2026-07-11 12:44:51 +02:00
parent c39330bffb
commit 8aa758ae64
13 changed files with 823 additions and 8 deletions
+53
View File
@@ -0,0 +1,53 @@
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.
export function initPanelView(opts: {
content: HTMLElement;
copyBtn: HTMLElement;
modeBtn: HTMLElement;
}): 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);
});
draw();
return {
set(text: string) {
rawText = text;
draw();
},
raw: () => rawText,
};
}
+27
View File
@@ -0,0 +1,27 @@
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")!,
});
view.set(await invoke<string>("panel_read", { project }));
await listen<string>("panel-update", (e) => view.set(e.payload));
// „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());
+72
View File
@@ -9,6 +9,8 @@ import "@fontsource/jetbrains-mono/600.css";
import "@fontsource/jetbrains-mono/700.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";
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
function showError(msg: string) {
@@ -529,3 +531,73 @@ window.addEventListener("resize", () => fit.fit());
await invoke("term_start", { project, rows: term.rows, cols: term.cols });
term.focus();
// --- Andockbares Entwurfs-Panel -------------------------------------------
// Der Skill schreibt Entwürfe in eine Datei; terminal.rs meldet neuen Inhalt
// per `panel-update`. Das Panel blendet sich dann ein — außer es ist gerade in
// ein eigenes Fenster abgelöst (`panel-detached`).
const panel = document.getElementById("panel")!;
const splitter = document.getElementById("splitter")!;
const panelContent = document.getElementById("panel-content")!;
// Panelfarben ans Theme koppeln.
panel.style.background = picked.header;
panel.style.color = theme.foreground;
splitter.style.background = picked.border;
(panel.querySelector(".panel-head") as HTMLElement).style.borderBottomColor =
picked.border;
const view = initPanelView({
content: panelContent,
copyBtn: document.getElementById("panel-copy")!,
modeBtn: document.getElementById("panel-mode")!,
});
let detached = false;
let hasContent = false;
function showPanel() {
panel.hidden = false;
splitter.hidden = false;
fit.fit();
}
function hidePanel() {
panel.hidden = true;
splitter.hidden = true;
fit.fit();
}
await listen<string>("panel-update", (e) => {
view.set(e.payload);
hasContent = true;
if (!detached) showPanel();
});
await listen("panel-detached", () => {
detached = true;
hidePanel();
});
await listen("panel-attached", () => {
detached = false;
if (hasContent) showPanel();
});
document
.getElementById("panel-detach")!
.addEventListener("click", () => invoke("open_panel_window", { project }));
document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
// Splitter: Panelbreite ziehen.
splitter.addEventListener("mousedown", (e) => {
e.preventDefault();
const move = (ev: MouseEvent) => {
const w = Math.max(240, Math.min(window.innerWidth - 200, window.innerWidth - ev.clientX));
panel.style.width = `${w}px`;
fit.fit();
};
const up = () => {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", up);
};
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
});