Skip to content
Open
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CoAgent
# CoAgent

Vendor-independent **BPOE middleware**: mandatory offloading (CoCache/CoTemp), heartbeat/guard with [OE:<glyph>], and multi-AI adapter.
**Scaffold only** — source-of-truth lives (for now) in:
Expand Down Expand Up @@ -73,3 +73,14 @@ Migration PRs will lift code/docs here; nothing moved yet.
- **Status panel:** https://rickballard.github.io/CoAgent/status.html (reads \status.json\)
- **Guardrails:** see \.coagent/guardrails/\ and smoke workflow.



# Disclaimer on Memory Handling & Service Interop

CoAgent deliberately externalizes memory and state, bypassing or overriding built-in AI memory services. This is a user-centric safeguard against corruption, vendor lock-in, and opacity.

We encourage all platform and container providers to evolve in parallel with these practices. Our approach is not intended to undermine or devalue their services, but to guarantee continuity, transparency, and reproducibility for **CoCivium-aligned users**.

Any overlap with vendor-patented or proprietary features is incidental. Our implementation exists only to protect user autonomy and ensure that CoCivium remains resilient regardless of service availability or policy changes.

We are open to collaboration and licensing discussions; in fact we **actively support forking and distributed ownership** of our products, features, and ideas; **but we will always protect the people before all else**, and thus **we reserve all available rights to retain parallel implementations** for redundancy and user protection.
44 changes: 44 additions & 0 deletions app/ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
(() => {
const input = document.querySelector("#ai-input");
const send = document.querySelector("#ai-send");
const buffer = document.querySelector("#ai-buffer");
if (!input || !send || !buffer) return;

let thinking = false;
const setThinking = (v) => {
thinking = v;
send.disabled = v;
input.dataset.thinking = v ? "1" : "0";
buffer.dataset.active = v ? "1" : "0";
};

input.addEventListener("input", ()=>{ if (thinking) buffer.textContent = input.value; });
input.addEventListener("keydown",(e)=>{
if (thinking && e.key === "Enter" && !e.shiftKey) { e.preventDefault(); return false; }
});

async function callAI(prompt){
// TODO wire real provider
await new Promise(r=>setTimeout(r, 1500));
return { ok:true, text:"OK" };
}

send.addEventListener("click", async ()=>{
if (thinking) return;
const prompt = input.value.trim();
if (!prompt) return;

try { window.CoLog?.log("ai_request_start", { chars: prompt.length }); } catch {}
setThinking(true); buffer.textContent = "";

let ok=false;
try {
const res = await callAI(prompt);
ok = !!res?.ok;
// TODO: render response
} finally {
setThinking(false);
try { window.CoLog?.log("ai_request_finish", { ok, remainingChars: input.value.length }); } catch {}
}
});
})();
17 changes: 17 additions & 0 deletions app/assets/sample-watermark.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* SAMPLE watermark (toggle with ?sample=1 or data-sample="1"; override text via ?wm=TEXT) */
body.is-sample::before{
content: attr(data-watermark, "SAMPLE");
position:fixed; inset:0; pointer-events:none;
display:flex; align-items:center; justify-content:center;
font:700 6rem/1 system-ui, Segoe UI, Arial;
color:#00000022; transform:rotate(-24deg); letter-spacing:.5rem;
z-index:2147483647;
}
@media (max-width:600px){
body.is-sample::before{ font-size:3rem; letter-spacing:.25rem; }
}

/* Fallback when no data-watermark attribute is present */
body.is-sample:not([data-watermark])::before{
content:"SAMPLE";
}
13 changes: 13 additions & 0 deletions app/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,16 @@ nav.tabs{display:flex;gap:6px;margin-top:8px}
nav.tabs a{padding:8px 10px;border:1px solid #e5e7eb;border-radius:10px;text-decoration:none;color:#0f172a;background:#fff}
nav.tabs a.active{border-color:var(--brand-primary);color:var(--brand-primary);background:#eef2ff}

<<<<<<< Updated upstream
#status-net[data-ok="1"]::before { content:"● "; color:#16a34a }
#status-net[data-ok="0"]::before { content:"● "; color:#ef4444 }
#status-export { margin-left:8px; text-decoration:underline; opacity:.85; }
#status-export:hover { opacity:1; }
=======
.panel { border:1px solid #e5e7eb; border-radius:12px; margin:.5rem 0; }
.panel-head { display:flex; align-items:center; justify-content:space-between; padding:.4rem .6rem; border-bottom:1px solid #eef2f7; border-radius:12px 12px 0 0; background:#fafafa; }
.panel-title { font-weight:600; opacity:.9; }
.panel-actions { display:flex; gap:.35rem; }
.panel-btn { border:1px solid #e5e7eb; border-radius:8px; padding:.2rem .45rem; cursor:pointer; }
.panel[data-ping="1"] .panel-head { outline:2px solid #93c5fd; transition:outline 180ms; }
>>>>>>> Stashed changes
4 changes: 4 additions & 0 deletions app/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"mode": "offline",
"sandboxUrl": ""
}
16 changes: 16 additions & 0 deletions app/help/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html><meta charset="utf-8">
<title>CoAgent • Help</title>
<link rel="icon" href="../assets/favicon.ico">
<link rel="stylesheet" href="../assets/style.css">
<header><img src="../assets/logo.png" alt="logo"><h1>Help • First-Run Guide</h1></header>
<main>
<div class="card">
<h2>Welcome</h2>
<p>Click <b>Start Guided Training</b> from Home. The rainbow bar means “working”.</p>
<ul>
<li><b>Sandbox Off</b>: everything stays local (demo mode).</li>
<li><b>Sandbox On</b>: submissions go to your CoCivium sandbox endpoint.</li>
</ul>
<p>You can change this via the toggle on the top bar.</p>
</div>
</main>
9 changes: 9 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h3 style="margin:0 0 6px 0;">What you’ll see</h3>
</ul>
</div>
</main>
<script src="./logger.js"></script>
<script>
const status = document.querySelector('#status');
document.querySelector('#selfcheck').onclick = () => {
Expand All @@ -35,3 +36,11 @@ <h3 style="margin:0 0 6px 0;">What you’ll see</h3>
document.querySelector('#help').href = 'https://rickballard.github.io/CoAgent/status.html'; // swap to HELP.md on Pages if desired
</script>









42 changes: 42 additions & 0 deletions app/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* logger.js — CoAgent event logger (localStorage ring buffer + export)
- Stores up to 500 events in localStorage under "coagent.events"
- Exposes window.CoLog.log(type, data) and window.CoLog.export()
*/
(() => {
const KEY = "coagent.events";
const MAX = 500;

function load() {
try { return JSON.parse(localStorage.getItem(KEY) || "[]"); }
catch { return []; }
}
function save(arr) {
try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch {}
}
function nowIso() {
const d=new Date();
const p=n=>String(n).padStart(2,"0");
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} `+
`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
function log(type, data) {
const evs = load();
evs.push({ t: nowIso(), type, data: data ?? null });
if (evs.length > MAX) evs.splice(0, evs.length - MAX);
save(evs);
}
function exportLog() {
const blob = new Blob([ JSON.stringify(load(), null, 2) ], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `coagent-events-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
setTimeout(()=>{ URL.revokeObjectURL(a.href); a.remove(); }, 100);
}

window.CoLog = { log, export: exportLog };

// stamp page load
log("page_load", { path: location.pathname, href: location.href });
})();
142 changes: 142 additions & 0 deletions app/panels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* panels.js — per-panel refresh/kill controls
Conventions:
- Each panel root has: class="panel" data-panel-id="status|ai|browser|terminal|..."
- Optional: data-title for button tooltip/label
- Add a child <div class="panel-head"></div> or we create one
Buttons:
- ↻ Soft refresh (no cache bust, re-run loaders)
- ⟲ Hard refresh (cache-bust + SW unregister + local state clear)
- ✖ Close (remove panel)
*/
(function () {
const q = (sel, r=document)=>r.querySelector(sel);
const qa = (sel, r=document)=>Array.from(r.querySelectorAll(sel));
const nowTag = ()=>Date.now().toString(36);

// Util: cache-busting URL
function bust(url) {
try {
const u = new URL(url, location.href);
u.searchParams.set("_cb", nowTag());
return u.toString();
} catch { return url + (url.includes("?") ? "&" : "?") + "_cb=" + nowTag(); }
}

async function unregisterServiceWorkers() {
if (!("serviceWorker" in navigator)) return;
try {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map(r => r.unregister().catch(()=>{})));
} catch {}
}

// Panel-specific soft refresh hooks (no page reload)
const softRefreshers = {
status: () => {
// Ask status.js to re-probe & tick
document.dispatchEvent(new CustomEvent("coagent:refresh:status"));
},
ai: () => {
// Nothing heavy; clear AI buffer if present
const buf = q("#ai-buffer"); if (buf) buf.textContent = "";
},
browser: (panel) => {
// If panel contains an iframe, reload it (no cache bust)
const f = q("iframe", panel); if (f) f.contentWindow?.location?.reload();
},
terminal: () => {
// For an embedded xterm/PS7 host, emit a custom event your terminal code listens to
document.dispatchEvent(new CustomEvent("coagent:refresh:terminal"));
}
};

// Panel-specific hard refresh hooks
const hardRefreshers = {
status: async () => {
// Clear minimal local caches used by status/logger
try { localStorage.removeItem("coagent.sandbox.mode"); } catch {}
document.dispatchEvent(new CustomEvent("coagent:refresh:status"));
},
ai: async () => {
// Nuke any local AI draft if you store it later
try { localStorage.removeItem("coagent.ai.draft"); } catch {}
const buf = q("#ai-buffer"); if (buf) buf.textContent = "";
},
browser: async (panel) => {
const f = q("iframe", panel);
if (f) { f.src = bust(f.src); return; }
// If it’s just a div, try a location bust of the whole page section
document.dispatchEvent(new CustomEvent("coagent:refresh:browser"));
},
terminal: async () => {
document.dispatchEvent(new CustomEvent("coagent:hardrefresh:terminal"));
}
};

function decoratePanel(panel) {
if (!panel || panel.dataset.decorated) return;
panel.dataset.decorated = "1";
panel.classList.add("panel");

let head = q(":scope > .panel-head", panel);
if (!head) {
head = document.createElement("div");
head.className = "panel-head";
head.innerHTML = `
<div class="panel-title"></div>
<div class="panel-actions">
<button class="panel-btn panel-refresh" title="Refresh (Ctrl+R)">↻</button>
<button class="panel-btn panel-hard" title="Hard refresh (Ctrl+Shift+R)">⟲</button>
<button class="panel-btn panel-close" title="Close">✖</button>
</div>`;
panel.prepend(head);
}
const title = q(":scope > .panel-head .panel-title", panel);
if (title && !title.textContent.trim()) {
title.textContent = panel.getAttribute("data-title") || panel.getAttribute("data-panel-id") || "Panel";
}

const id = panel.dataset.panelId || "unknown";
const refreshBtn = q(".panel-refresh", head);
const hardBtn = q(".panel-hard", head);
const closeBtn = q(".panel-close", head);

refreshBtn?.addEventListener("click", () => {
(softRefreshers[id] || (()=>{}))(panel);
// visual ping
panel.setAttribute("data-ping","1");
setTimeout(()=>panel.removeAttribute("data-ping"), 250);
try { window.CoLog?.log("panel_refresh", { id, mode:"soft" }); } catch{}
});

hardBtn?.addEventListener("click", async () => {
await unregisterServiceWorkers();
(hardRefreshers[id] || (()=>{}))(panel);
panel.setAttribute("data-ping","1");
setTimeout(()=>panel.removeAttribute("data-ping"), 250);
try { window.CoLog?.log("panel_refresh", { id, mode:"hard" }); } catch{}
});

closeBtn?.addEventListener("click", () => {
try { window.CoLog?.log("panel_close", { id }); } catch {}
panel.remove();
});

// Keyboard shortcuts scoped to panel focus
panel.addEventListener("keydown", (e) => {
const ctrl = e.ctrlKey || e.metaKey;
if (ctrl && !e.shiftKey && e.key.toLowerCase()==="r") { e.preventDefault(); refreshBtn?.click(); }
if (ctrl && e.shiftKey && e.key.toLowerCase()==="r") { e.preventDefault(); hardBtn?.click(); }
});
}

// Auto-decorate any .panel on load
qa(".panel").forEach(decoratePanel);

// Observe for dynamically added panels (e.g., new browser tabs)
new MutationObserver(muts => {
muts.forEach(m => m.addedNodes.forEach(n => {
if (n.nodeType===1 && n.classList?.contains("panel")) decoratePanel(n);
}));
}).observe(document.body, { childList:true, subtree:true });
})();
Loading