import { useState, useEffect, useCallback, useRef } from "react"; import { SimpleDiffEditor } from "../components/editor/SimpleEditor"; import { api } from "../services/api"; import { useWebSocket } from "../hooks/useWebSocket"; import { Eye, EyeOff, FileCode, Check, X, RefreshCw, Trash2, ArrowUpCircle, } from "lucide-react"; interface ChangeEntry { filename: string; gffType: string; repoPath: string | null; timestamp: number; } interface DiffData { original: string; modified: string; filename: string; } function formatTimestamp(ts: number): string { return new Date(ts).toLocaleTimeString(); } export function Toolset() { const { subscribe } = useWebSocket(); const [active, setActive] = useState(false); const [changes, setChanges] = useState([]); const [selected, setSelected] = useState>(new Set()); const [diffData, setDiffData] = useState(null); const [loading, setLoading] = useState(false); const [lastChange, setLastChange] = useState(null); const diffContainerRef = useRef(null); const refresh = useCallback(async () => { try { const status = await api.toolset.status(); setActive(status.active); const list = await api.toolset.changes(); setChanges(list); if (list.length > 0) { setLastChange(Math.max(...list.map((c) => c.timestamp))); } } catch { // backend may not be reachable yet } }, []); useEffect(() => { refresh(); }, [refresh]); useEffect(() => { const unsubs = [ subscribe("toolset:change", (event) => { const data = event.data as ChangeEntry; setChanges((prev) => { const idx = prev.findIndex((c) => c.filename === data.filename); if (idx >= 0) { const next = [...prev]; next[idx] = data; return next; } return [...prev, data]; }); setLastChange(data.timestamp); }), subscribe("toolset:applied", (event) => { const { filename } = event.data as { filename: string }; setChanges((prev) => prev.filter((c) => c.filename !== filename)); setSelected((prev) => { const next = new Set(prev); next.delete(filename); return next; }); if (diffData?.filename === filename) setDiffData(null); }), subscribe("toolset:discarded", (event) => { const { filename } = event.data as { filename: string }; setChanges((prev) => prev.filter((c) => c.filename !== filename)); setSelected((prev) => { const next = new Set(prev); next.delete(filename); return next; }); if (diffData?.filename === filename) setDiffData(null); }), subscribe("toolset:discarded-all", () => { setChanges([]); setSelected(new Set()); setDiffData(null); }), subscribe("toolset:watcher:started", () => setActive(true)), subscribe("toolset:watcher:stopped", () => setActive(false)), ]; return () => unsubs.forEach((u) => u()); }, [subscribe, diffData?.filename]); const toggleSelect = (filename: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(filename)) next.delete(filename); else next.add(filename); return next; }); }; const toggleAll = () => { if (selected.size === changes.length) { setSelected(new Set()); } else { setSelected(new Set(changes.map((c) => c.filename))); } }; const viewDiff = async (change: ChangeEntry) => { setLoading(true); try { const full = await api.toolset.getChange(change.filename); let original = ""; if (change.repoPath) { try { const { content } = await api.editor.readFile( "nwn-module", change.repoPath, ); original = content; } catch { // file doesn't exist in repo yet } } setDiffData({ original, modified: full.jsonContent ?? "", filename: change.filename, }); } catch (err) { console.error("Failed to load diff:", err); } finally { setLoading(false); } }; const handleStart = async () => { await api.toolset.start(); setActive(true); }; const handleStop = async () => { await api.toolset.stop(); setActive(false); }; const handleApplySelected = async () => { if (selected.size === 0) return; await api.toolset.apply(Array.from(selected)); refresh(); }; const handleApplyAll = async () => { await api.toolset.applyAll(); refresh(); }; const handleDiscardSelected = async () => { if (selected.size === 0) return; await api.toolset.discard(Array.from(selected)); refresh(); }; const handleDiscardAll = async () => { if (!window.confirm("Discard all changes? This cannot be undone.")) return; await api.toolset.discardAll(); refresh(); }; return (
{/* Page heading */}

Toolset

Watch for NWN Toolset changes and apply them to the repository

{/* Watcher status card */}
{active ? ( ) : ( )} {active ? "Active" : "Inactive"}
{changes.length} pending change{changes.length !== 1 && "s"} {lastChange && ( Last change: {formatTimestamp(lastChange)} )}
{/* Main content area */}
{/* Changes card */}
{/* Card header with action bar */}
Pending Changes {changes.length > 0 && ( {selected.size} of {changes.length} selected )}
{changes.length > 0 && (
)}
{/* Table or empty state */}
{changes.length === 0 ? (
{active ? ( <> Watching for changes in temp0/... ) : ( <> Start the watcher to detect Toolset changes )}
) : ( {changes.map((change) => ( viewDiff(change)} style={{ borderBottom: "1px solid var(--forge-border)", cursor: "pointer", backgroundColor: diffData?.filename === change.filename ? "var(--forge-accent-subtle)" : undefined, }} > ))}
0 } onChange={toggleAll} style={{ cursor: "pointer" }} /> Filename Type Repo Path Time
{ e.stopPropagation(); toggleSelect(change.filename); }} onClick={(e) => e.stopPropagation()} style={{ cursor: "pointer" }} /> {change.filename} {change.gffType} {change.repoPath ?? "—"} {formatTimestamp(change.timestamp)}
)}
{/* Diff viewer panel */} {diffData && (
{/* Diff header */}
{diffData.filename} {loading && ( Loading... )}
{/* Diff content */}
)}
); }