/* App shell: 4 resizable columns, keyboard shortcuts, copy-reference, status bar */
function Splitter({ orientation = "v", onDelta }) {
const [drag, setDrag] = useState(false);
function down(e) {
e.preventDefault();
let last = { x: e.clientX, y: e.clientY };
setDrag(true);
document.body.style.cursor = orientation === "v" ? "col-resize" : "row-resize";
document.body.style.userSelect = "none";
function mv(ev) {
onDelta(ev.clientX - last.x, ev.clientY - last.y);
last = { x: ev.clientX, y: ev.clientY };
}
function up() {
setDrag(false);
document.body.style.cursor = ""; document.body.style.userSelect = "";
document.removeEventListener("mousemove", mv); document.removeEventListener("mouseup", up);
}
document.addEventListener("mousemove", mv); document.addEventListener("mouseup", up);
}
return
;
}
function RightColumn({ width }) {
const [topFrac, setTopFrac] = useState(0.52);
const ref = useRef(null);
function delta(dx, dy) {
const h = ref.current ? ref.current.clientHeight : 600;
setTopFrac((f) => Math.max(0.18, Math.min(0.82, (f * h + dy) / h)));
}
const agent = useMemo(() => agentSeed(), []);
const shell = useMemo(() => shellSeed(), []);
return (
);
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function ancestors(path) {
const parts = path.split("/"); const out = [];
for (let i = 1; i < parts.length; i++) out.push(parts.slice(0, i).join("/"));
return out;
}
function initialOpenDirs(node, set) {
if (node.type === "dir") {
if (node.open && node.path) set.add(node.path);
(node.children || []).forEach((c) => initialOpenDirs(c, set));
}
return set;
}
function App() {
const changeMap = useMemo(() => Object.fromEntries(PROJECT.changes.map((c) => [c.path, c.status])), []);
const changeSet = useMemo(() => new Set(PROJECT.changes.map((c) => c.path)), []);
const [tabs, setTabs] = useState([
{ path: "src/Http/Controller/UserController.php" },
{ path: "public/assets/app.js" },
{ path: "src/types/api.ts" },
]);
const [active, setActive] = useState("src/Http/Controller/UserController.php");
const [tabMode, setTabMode] = useState({ "src/Http/Controller/UserController.php": "diff" });
const [openDirs, setOpenDirs] = useState(() => initialOpenDirs(PROJECT.tree, new Set()));
const [cursor, setCursor] = useState({ path: "src/Http/Controller/UserController.php", line: 1, col: 1 });
const [selection, setSelection] = useState(null);
const [overlay, setOverlay] = useState(null);
const [menu, setMenu] = useState(null);
const [toasts, setToasts] = useState([]);
const [splitFor, setSplitFor] = useState(null);
const [staged, setStaged] = useState(() => new Set(["src/Service/PaymentService.php", "config/app.json"]));
const [committed, setCommitted] = useState(() => new Set());
const [commitMsg, setCommitMsg] = useState("");
const [passPopup, setPassPopup] = useState(null);
const [gitW, setGitW] = useState(232);
const [treeW, setTreeW] = useState(244);
const [rightW, setRightW] = useState(444);
function toast(title, ref) {
const id = lid();
setToasts((t) => [...t, { id, title, ref }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2300);
}
async function copyText(text, label) {
try { await navigator.clipboard.writeText(text); }
catch (e) {
const ta = document.createElement("textarea"); ta.value = text;
ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta);
ta.select(); try { document.execCommand("copy"); } catch (_) {} ta.remove();
}
toast(label || "Copied reference", text);
}
const toggleDir = useCallback((p) => {
setOpenDirs((s) => { const n = new Set(s); n.has(p) ? n.delete(p) : n.add(p); return n; });
}, []);
function reveal(path) {
setOpenDirs((s) => { const n = new Set(s); ancestors(path).forEach((a) => n.add(a)); return n; });
}
const stage = (p) => setStaged((s) => { const n = new Set(s); n.add(p); return n; });
const unstage = (p) => setStaged((s) => { const n = new Set(s); n.delete(p); return n; });
const stageAll = () => setStaged(new Set(PROJECT.changes.filter((c) => !committed.has(c.path)).map((c) => c.path)));
const unstageAll = () => setStaged(new Set());
function commit() {
const list = PROJECT.changes.filter((c) => staged.has(c.path) && !committed.has(c.path));
if (!list.length || !commitMsg.trim()) return;
setCommitted((prev) => { const n = new Set(prev); list.forEach((c) => n.add(c.path)); return n; });
setStaged(new Set());
const msg = commitMsg.trim();
setCommitMsg("");
toast(`Committed ${list.length} file${list.length > 1 ? "s" : ""}`, msg.length > 34 ? msg.slice(0, 34) + "…" : msg);
}
function openFile(path, opts = {}) {
const changed = !!PROJECT.diffs[path];
setTabs((t) => t.some((x) => x.path === path) ? t : [...t, { path }]);
setActive(path);
setTabMode((m) => ({ ...m, [path]: opts.diff && changed ? "diff" : (m[path] || (changed ? "diff" : "code")) }));
reveal(path);
if (opts.line) {
// show the current/updated file so line numbers map to search hits
setSplitFor(null);
setTabMode((m) => ({ ...m, [path]: changed ? "updated" : "code" }));
setCursor({ path, line: opts.line, col: 1 });
setSelection(null);
setTimeout(() => {
const row = document.querySelector('.editor .ln-row[data-line="' + opts.line + '"]');
if (row) { const ed = row.closest(".editor"); const er = ed.getBoundingClientRect(), rr = row.getBoundingClientRect(); ed.scrollTop += (rr.top - er.top) - ed.clientHeight / 2; }
}, 70);
}
}
function closeTab(path) {
setTabs((t) => {
const ix = t.findIndex((x) => x.path === path);
const next = t.filter((x) => x.path !== path);
if (path === active) {
const fallback = next[ix] || next[ix - 1] || next[next.length - 1];
setActive(fallback ? fallback.path : null);
}
return next;
});
}
// ---- context menus ----
function openMenu(e, target) {
e.preventDefault(); e.stopPropagation();
const sparkSend = (ref) => ({ icon: Icon.spark({}), label: "Send reference to agent", onClick: () => { window.dispatchEvent(new CustomEvent("agentPaste", { detail: ref })); toast("Passed to agent", ref); } });
if (target.kind === "editor") {
const ref = target.sel ? `${target.path}:${target.sel.start}-${target.sel.end}` : `${target.path}:${target.line}`;
const mx = e.clientX, my = e.clientY;
setMenu({
x: mx, y: my, note: ref,
items: [
{ primary: true, icon: Icon.copy({}), label: "Copy reference", onClick: () => copyText(ref) },
{ icon: Icon.spark({}), label: "Pass on to Agent", onClick: () => setPassPopup({ x: mx, y: my, ref }) },
],
});
} else {
const isDir = target.kind === "dir";
const ref = isDir ? target.path + "/" : target.path;
const name = target.path.split("/").pop();
const items = [
{ primary: true, icon: Icon.copy({}), label: "Copy reference", onClick: () => copyText(ref) },
sparkSend(ref),
{ icon: Icon.copy({}), label: isDir ? "Copy folder path" : "Copy file name", onClick: () => copyText(isDir ? target.path : name, "Copied") },
];
if (!isDir) {
items.push({ sep: true });
if (target.kind === "git") {
const isStaged = staged.has(target.path);
items.push(isStaged
? { icon: Icon.minus({}), label: "Unstage changes", onClick: () => unstage(target.path) }
: { icon: Icon.plus({}), label: "Stage changes", onClick: () => stage(target.path) });
items.push({ icon: Icon.diff({}), label: "Open diff", onClick: () => openFile(target.path, { diff: true }) });
}
items.push({ icon: Icon.file({}), label: "Open file", onClick: () => openFile(target.path) });
items.push({ icon: Icon.reveal({}), label: "Reveal in Explorer", onClick: () => reveal(target.path) });
}
setMenu({ x: e.clientX, y: e.clientY, note: ref, items });
}
}
// ---- shortcuts ----
useEffect(() => {
function onKey(e) {
const meta = e.metaKey || e.ctrlKey;
if (meta && e.key.toLowerCase() === "f") { e.preventDefault(); setOverlay("search"); }
else if (meta && e.key.toLowerCase() === "w") { e.preventDefault(); if (active) closeTab(active); }
else if (e.key === "Escape") { if (splitFor) setSplitFor(null); else { setOverlay(null); setMenu(null); } }
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [active, splitFor]);
const MODE_LABEL = { original: "orig", updated: "upd", diff: "diff", code: "" };
const MODE_WORD = { original: "Original", updated: "Updated", diff: "Diff" };
const resolvedTabs = tabs.map((t) => {
const changed = !!PROJECT.diffs[t.path];
const m = tabMode[t.path] || (changed ? "diff" : "code");
return { ...t, changed, modeLabel: splitFor === t.path ? "split" : MODE_LABEL[m] };
});
const mode = tabMode[active] || (PROJECT.diffs[active] ? "diff" : "code");
const totals = PROJECT.changes.reduce((a, c) => ({ add: a.add + c.add, del: a.del + c.del }), { add: 0, del: 0 });
const activeLang = active ? HL.langLabel(active) : "";
const crumb = active ? active.split("/") : [];
return (
{/* title bar */}
{Icon.spark({ style: { color: "var(--accent)" } })}Helder—{PROJECT.name}
{active && (
{crumb.map((s, i) => ({i > 0 && › }{s}))}
)}
{/* workbench */}
setGitW((w) => clamp(w + dx, 160, 460))} />
setTreeW((w) => clamp(w + dx, 160, 520))} />
{ setTabMode((mm) => ({ ...mm, [active]: m })); setSplitFor(null); }}
onActivate={setActive} onClose={closeTab} onContext={openMenu}
onSplit={(p) => setSplitFor(p)} splitOpen={splitFor === active}
cursor={cursor} selection={selection} setCursor={setCursor} setSelection={setSelection} />
setRightW((w) => clamp(w - dx, 280, 780))} />
{/* status bar */}
{Icon.branch({ width: 12, height: 12 })}{PROJECT.branch}
+{totals.add} −{totals.del}
{active &&
{selection && selection.path === active && selection.start !== selection.end ? `${selection.end - selection.start + 1} lines selected` : `Ln ${cursor.path === active ? cursor.line : 1}, Col ${cursor.path === active ? cursor.col : 1}`}
}
{active &&
Spaces: 4
}
{active &&
UTF-8
}
{active &&
{activeLang}
}
{active && PROJECT.diffs[active] &&
{splitFor === active ? "Split" : (MODE_WORD[mode] || "")}
}
{/* overlays */}
{splitFor &&
setSplitFor(null)} onContext={openMenu} />}
{passPopup && {
const line = (text && text.trim() ? text.trim() + " " : "") + passPopup.ref;
window.dispatchEvent(new CustomEvent("agentPaste", { detail: line }));
setPassPopup(null);
toast("Passed to agent", passPopup.ref);
}}
onCancel={() => setPassPopup(null)} />}
{overlay === "search" && openFile(p, { line: n })} onClose={() => setOverlay(null)} changeSet={changeSet} />}
{menu && setMenu(null)} />}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();