Embed umbrella graph in workspace panel
Some checks failed
plugin-tests / test (push) Failing after 5s
upstream-drift / drift (push) Failing after 5s

This commit is contained in:
Svrnty 2026-05-26 06:34:50 -04:00
parent 28ffa92f6f
commit c0ff59097c
11 changed files with 373 additions and 26 deletions

View File

@ -2,7 +2,7 @@
**Upstream version:** v0.51.118
**Plugin version:** 0.5.0
**Total dependencies:** 32 (23 public API · 0 forced internal · 9 frontend)
**Total dependencies:** 34 (25 public API · 0 forced internal · 9 frontend)
> **Auto-generated by `scripts/ast-connection-map.py`. Do not hand-edit.**
> To change a justification, edit the `# CONNECTION:` comment above the
@ -23,6 +23,8 @@
| `plugin.py:43` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/adwright.js")` |
| `plugin.py:45` | `api.inject_stylesheet` | `api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/bte.css")` |
| `plugin.py:46` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/bte.js")` |
| `plugin.py:48` | `api.inject_stylesheet` | `api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/umbrella_inline.css")` |
| `plugin.py:49` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/umbrella_inline.js")` |
| `routes/adwright.py:68` | `api.logger` | `log = api.logger("svrnty.routes.adwright")` |
| `routes/adwright.py:69` | `api.register_route` | `api.register_route(` |
| `routes/adwright.py:71` | `api.register_route` | `api.register_route(` |
@ -52,13 +54,13 @@ _None. Plugin uses only the public API._ ✓
| File | Line | URL |
|---|---|---|
| `static/bte.js` | 329 | `/api/command/requestPhotoshoot` |
| `static/bte.js` | 360 | `/api/query/assetDtos` |
| `static/bte.js` | 372 | `/api/assets/` |
| `static/bte.js` | 481 | `/api/command/rateAsset` |
| `static/bte.js` | 365 | `/api/command/requestPhotoshoot` |
| `static/bte.js` | 396 | `/api/query/assetDtos` |
| `static/bte.js` | 408 | `/api/assets/` |
| `static/bte.js` | 517 | `/api/command/rateAsset` |
| `static/adwright.js` | 176 | `/api/profile/switch` |
| `static/adwright.js` | 197 | `/api/profile/active` |
| `static/adwright.js` | 606 | `/api/adwright/provision-creds` |
| `static/umbrella.js` | 41 | `/api/umbrella` |
| `static/umbrella.js` | 57 | `/api/umbrella` |
| `static/app.js` | 165 | `/api/vault/status` |

View File

@ -33,10 +33,12 @@ assets:
- /plugins/svrnty/svrnty_nav.js
- /plugins/svrnty/adwright.js
- /plugins/svrnty/bte.js
- /plugins/svrnty/umbrella_inline.js
stylesheets:
- /plugins/svrnty/app.css
- /plugins/svrnty/adwright.css
- /plugins/svrnty/bte.css
- /plugins/svrnty/umbrella_inline.css
# Routes this plugin registers at load time (declarative cross-check vs runtime).
# Each row maps to a routes/<file>.py.

View File

@ -44,6 +44,9 @@ def register(api):
# BTE Command Center panel — same pattern (main.svrnty-showing-bte).
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/bte.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/bte.js")
# Inline Umbrella graph for the Hermes Workspace right panel.
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/umbrella_inline.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/umbrella_inline.js")
log.info("static + assets wired at /plugins/%s/", STATIC_PREFIX)
# Routes — each feature lives in its own module under routes/.

View File

@ -22,19 +22,10 @@
'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
bte:
'<path d="M12 2l1.8 5.6L19.4 9.4l-4.5 3.3 1.7 5.7L12 15l-4.6 3.4 1.7-5.7L4.6 9.4l5.6-1.8L12 2z"/>',
graph:
'<circle cx="5" cy="12" r="2"/><circle cx="12" cy="5" r="2"/><circle cx="19" cy="12" r="2"/><circle cx="12" cy="19" r="2"/><path d="M6.7 10.6l3.9-4"/><path d="M13.4 6.6l3.9 4"/><path d="M17.3 13.4l-3.9 4"/><path d="M10.6 17.4l-3.9-4"/><path d="M7 12h10"/>',
};
const TABS = [
{ id: "adwright", label: "Adwright", tooltip: "Adwright — marketing intelligence" },
{ id: "bte", label: "BTE", tooltip: "BTE — brand creative studio" },
{
id: "project-graph",
label: "Project Graph",
tooltip: "Project Graph — open workspace graph",
href: "/plugins/svrnty/umbrella.html",
icon: "graph",
},
];
function _svg(iconPath, size, stroke) {
@ -84,10 +75,6 @@
btn.innerHTML = _svg(ICONS[t.icon || t.id], c.size, c.stroke);
btn.addEventListener("click", () => {
LOG("clicked:", t.id);
if (t.href) {
window.open(t.href, "_blank", "noopener,noreferrer");
return;
}
if (typeof window.switchPanel === "function") {
window.switchPanel(t.id, { fromRailClick: true });
} else {
@ -116,7 +103,7 @@
if (typeof window.switchPanel !== "function") return false;
if (window.switchPanel.__svrntyWrapped) return true;
const original = window.switchPanel;
const OUR_IDS = TABS.filter((t) => !t.href).map((t) => t.id);
const OUR_IDS = TABS.map((t) => t.id);
async function wrapped(name, opts) {
const result = await original(name, opts);

View File

@ -159,3 +159,56 @@
display: flex; justify-content: space-between; align-items: center;
}
.umbrella-footer a { color: var(--accent); }
body.umbrella-inline {
margin: 0;
background: var(--bg);
}
body.umbrella-inline .umbrella-root {
position: fixed;
inset: 0;
z-index: 1;
}
body.umbrella-inline .umbrella-header {
padding: 8px;
grid-template-columns: 1fr;
gap: 6px;
}
body.umbrella-inline .umbrella-header h1 {
display: none;
}
body.umbrella-inline .umbrella-stats {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body.umbrella-inline .umbrella-controls {
flex-wrap: wrap;
gap: 5px;
}
body.umbrella-inline .umbrella-controls input[type=search] {
width: 100%;
box-sizing: border-box;
padding: 5px 8px;
font-size: 12px;
}
body.umbrella-inline .umbrella-controls button,
body.umbrella-inline .chip {
padding: 4px 7px;
font-size: 10px;
}
body.umbrella-inline .umbrella-filters,
body.umbrella-inline .umbrella-disclosure {
max-height: 58px;
overflow: auto;
}
body.umbrella-inline .umbrella-footer {
display: none;
}
body.umbrella-inline .umbrella-side {
width: 100%;
}
body.umbrella-inline .side-body {
max-height: 38vh;
}

View File

@ -7,6 +7,11 @@
<link rel="stylesheet" href="/plugins/svrnty/umbrella.css" />
</head>
<body>
<script>
if (new URLSearchParams(location.search).get("inline") === "1") {
document.body.classList.add("umbrella-inline");
}
</script>
<div id="svrnty-umbrella" class="umbrella-root">
<header class="umbrella-header">
<h1>Cortex-OS Umbrella</h1>

View File

@ -0,0 +1,28 @@
/* Inline Umbrella graph mounted into Hermes WebUI's Workspace right panel. */
.svrnty-graph-btn.active {
background: var(--accent-bg);
color: var(--accent-text);
}
.svrnty-umbrella-inline-surface {
display: none;
flex: 1;
min-height: 0;
background: var(--bg, #0f1115);
overflow: hidden;
}
.svrnty-umbrella-inline-surface[data-open="true"] {
display: flex;
flex-direction: column;
}
.svrnty-umbrella-inline-frame {
width: 100%;
height: 100%;
flex: 1;
min-height: 0;
border: 0;
background: #0f1115;
}

210
static/umbrella_inline.js Normal file
View File

@ -0,0 +1,210 @@
// umbrella_inline.js — mounts the Cortex-OS graph inside Hermes WebUI's
// Workspace right panel. Plugin-only shim; the standalone page remains a
// fallback at /plugins/svrnty/umbrella.html.
(function () {
"use strict";
if (window.__svrntyUmbrellaInlineLoaded) return;
window.__svrntyUmbrellaInlineLoaded = true;
const LOG = (...a) => console.log("[svrnty-umbrella-inline]", ...a);
const ICON =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" ' +
'stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
'stroke-linejoin="round" aria-hidden="true">' +
'<circle cx="5" cy="12" r="2"/><circle cx="12" cy="5" r="2"/>' +
'<circle cx="19" cy="12" r="2"/><circle cx="12" cy="19" r="2"/>' +
'<path d="M6.7 10.6l3.9-4"/><path d="M13.4 6.6l3.9 4"/>' +
'<path d="M17.3 13.4l-3.9 4"/><path d="M10.6 17.4l-3.9-4"/>' +
'<path d="M7 12h10"/></svg>';
let surface = null;
let graphOpen = false;
let previousHeading = "Workspace";
let previousPanelWidth = "";
let openedPanelForGraph = false;
function $(id) {
return document.getElementById(id);
}
function isWorkspacePanelOpen() {
const htmlState = document.documentElement.dataset.workspacePanel;
const panel = document.querySelector(".rightpanel");
return htmlState === "open" && !!panel && getComputedStyle(panel).pointerEvents !== "none";
}
function ensureSurface() {
if (surface && document.body.contains(surface)) return surface;
const panel = document.querySelector(".rightpanel");
const preview = $("previewArea");
if (!panel || !preview) return null;
surface = document.createElement("div");
surface.id = "svrntyUmbrellaInlineSurface";
surface.className = "svrnty-umbrella-inline-surface";
surface.dataset.open = "false";
surface.innerHTML =
'<iframe class="svrnty-umbrella-inline-frame" ' +
'title="Project graph" src="/plugins/svrnty/umbrella.html?inline=1"></iframe>';
preview.insertAdjacentElement("afterend", surface);
return surface;
}
function setWorkspaceSurfaces(mode) {
const fileTree = $("fileTree");
const empty = $("wsEmptyState");
const preview = $("previewArea");
const breadcrumb = $("breadcrumbBar");
const graph = ensureSurface();
if (!graph) return;
const showGraph = mode === "graph";
if (fileTree) fileTree.style.display = showGraph ? "none" : "";
if (empty) empty.style.display = showGraph ? "none" : empty.style.display;
if (preview) preview.style.display = showGraph ? "none" : "";
if (breadcrumb) breadcrumb.style.display = showGraph ? "none" : breadcrumb.style.display;
graph.dataset.open = showGraph ? "true" : "false";
}
function syncHeader(open) {
const heading = $("workspacePanelHeading");
const button = $("btnSvrntyWorkspaceGraph");
if (heading) {
if (open) {
previousHeading = heading.textContent || "Workspace";
heading.textContent = "Project Graph";
heading.title = "Project Graph";
} else {
heading.textContent = previousHeading || "Workspace";
heading.title = "Workspace root";
}
}
if (button) {
button.classList.toggle("active", open);
button.setAttribute("aria-pressed", open ? "true" : "false");
}
}
function resizeGraphSoon() {
const frame = surface && surface.querySelector("iframe");
if (!frame) return;
setTimeout(() => {
try {
const cy = frame.contentWindow && frame.contentWindow.cy;
if (cy) {
cy.resize();
cy.fit(null, 24);
}
} catch (_) {}
}, 250);
}
function expandPanelForGraph() {
const panel = document.querySelector(".rightpanel");
if (!panel) return;
previousPanelWidth = panel.style.width || "";
const current = panel.getBoundingClientRect().width || 0;
if (current < 480) {
panel.style.width = "500px";
}
}
function restorePanelWidth() {
const panel = document.querySelector(".rightpanel");
if (!panel) return;
panel.style.width = previousPanelWidth;
previousPanelWidth = "";
}
function openWorkspaceGraph() {
if (graphOpen) {
resizeGraphSoon();
return;
}
openedPanelForGraph = !isWorkspacePanelOpen();
if (typeof window.openWorkspacePanel === "function") {
window.openWorkspacePanel("browse");
} else if (typeof window.toggleWorkspacePanel === "function") {
window.toggleWorkspacePanel(true);
}
expandPanelForGraph();
graphOpen = true;
setWorkspaceSurfaces("graph");
syncHeader(true);
resizeGraphSoon();
LOG("opened inline graph");
}
function closeWorkspaceGraph() {
if (!graphOpen) return false;
graphOpen = false;
setWorkspaceSurfaces("workspace");
syncHeader(false);
restorePanelWidth();
if (openedPanelForGraph && typeof window.closeWorkspacePanel === "function") {
window.closeWorkspacePanel();
} else if (typeof window.renderBreadcrumb === "function") {
window.renderBreadcrumb();
}
openedPanelForGraph = false;
LOG("closed inline graph");
return true;
}
function injectButton() {
const actions = document.querySelector(".rightpanel .panel-actions");
if (!actions || $("btnSvrntyWorkspaceGraph")) return !!actions;
const refresh = $("btnRefreshPanel");
const btn = document.createElement("button");
btn.className = "panel-icon-btn has-tooltip has-tooltip--bottom svrnty-graph-btn";
btn.id = "btnSvrntyWorkspaceGraph";
btn.type = "button";
btn.dataset.tooltip = "Project graph";
btn.setAttribute("aria-label", "Open project graph");
btn.setAttribute("aria-pressed", "false");
btn.innerHTML = ICON;
btn.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
openWorkspaceGraph();
});
if (refresh && refresh.parentElement === actions) {
refresh.insertAdjacentElement("afterend", btn);
} else {
actions.appendChild(btn);
}
ensureSurface();
return true;
}
function interceptCloseButton() {
const close = $("btnClearPreview");
if (!close || close.dataset.svrntyGraphCloseBound === "1") return !!close;
close.dataset.svrntyGraphCloseBound = "1";
close.addEventListener("click", (event) => {
if (!graphOpen) return;
event.preventDefault();
event.stopImmediatePropagation();
closeWorkspaceGraph();
}, true);
return true;
}
function init() {
const ok = injectButton() && interceptCloseButton();
if (!ok) {
requestAnimationFrame(init);
return;
}
window.SvrntyUmbrellaInline = {
open: openWorkspaceGraph,
close: closeWorkspaceGraph,
isOpen: () => graphOpen,
};
LOG("ready");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@ -66,6 +66,8 @@ def test_loader_register_wires_our_plugin(loader, monkeypatch):
assert "svrnty" in loader._STATIC
assert "/plugins/svrnty/app.css" in loader._STYLESHEETS
assert "/plugins/svrnty/app.js" in loader._SCRIPTS
assert "/plugins/svrnty/umbrella_inline.css" in loader._STYLESHEETS
assert "/plugins/svrnty/umbrella_inline.js" in loader._SCRIPTS
# Audio processor for voice-message transcription
assert len(loader._AUDIO_PROCESSORS) == 1

View File

@ -5,13 +5,14 @@ from pathlib import Path
NAV_JS = Path(__file__).resolve().parents[2] / "static" / "svrnty_nav.js"
def test_project_graph_nav_opens_umbrella_page_in_new_tab():
def test_project_graph_is_not_in_left_nav():
src = NAV_JS.read_text()
assert "Project Graph" in src
assert "/plugins/svrnty/umbrella.html" in src
assert 'window.open(t.href, "_blank", "noopener,noreferrer")' in src
assert "Project Graph" not in src
assert "/plugins/svrnty/umbrella.html" not in src
assert "window.open" not in src
def test_project_graph_does_not_participate_in_panel_switching():
def test_svrnty_tabs_participate_in_panel_switching():
src = NAV_JS.read_text()
assert "TABS.filter((t) => !t.href).map((t) => t.id)" in src
assert "const TABS = [" in src
assert "const OUR_IDS = TABS.map((t) => t.id)" in src

View File

@ -0,0 +1,54 @@
"""Static checks for the inline Workspace-panel umbrella graph integration."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
INLINE_JS = ROOT / "static" / "umbrella_inline.js"
INLINE_CSS = ROOT / "static" / "umbrella_inline.css"
UMBRELLA_HTML = ROOT / "static" / "umbrella.html"
UMBRELLA_CSS = ROOT / "static" / "umbrella.css"
PLUGIN = ROOT / "plugin.py"
def test_plugin_injects_inline_umbrella_assets():
src = PLUGIN.read_text()
assert "/plugins/{STATIC_PREFIX}/umbrella_inline.css" in src
assert "/plugins/{STATIC_PREFIX}/umbrella_inline.js" in src
def test_inline_graph_targets_workspace_right_panel():
src = INLINE_JS.read_text()
assert "btnSvrntyWorkspaceGraph" in src
assert '.rightpanel .panel-actions' in src
assert "Project graph" in src
assert "svrntyUmbrellaInlineSurface" in src
assert "/plugins/svrnty/umbrella.html?inline=1" in src
assert "window.open(" not in src
def test_inline_graph_uses_right_panel_mode_switching():
src = INLINE_JS.read_text()
assert "openWorkspaceGraph" in src
assert "closeWorkspaceGraph" in src
assert "openWorkspacePanel" in src
assert "expandPanelForGraph" in src
assert "restorePanelWidth" in src
assert 'panel.style.width = "500px"' in src
assert "btnClearPreview" in src
assert "stopImmediatePropagation" in src
def test_inline_graph_has_panel_surface_styles():
src = INLINE_CSS.read_text()
assert ".svrnty-umbrella-inline-surface" in src
assert '.svrnty-umbrella-inline-surface[data-open="true"]' in src
assert ".svrnty-umbrella-inline-frame" in src
def test_standalone_umbrella_supports_inline_mode():
html = UMBRELLA_HTML.read_text()
css = UMBRELLA_CSS.read_text()
assert 'get("inline") === "1"' in html
assert "umbrella-inline" in html
assert "body.umbrella-inline .umbrella-header" in css
assert "body.umbrella-inline .umbrella-footer" in css