diff --git a/CONNECTION-MAP.md b/CONNECTION-MAP.md index dc3d189..282eb1d 100644 --- a/CONNECTION-MAP.md +++ b/CONNECTION-MAP.md @@ -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` | diff --git a/manifest.yaml b/manifest.yaml index 4ffa92d..6434247 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -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/.py. diff --git a/plugin.py b/plugin.py index e991b62..3bb8eb1 100644 --- a/plugin.py +++ b/plugin.py @@ -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/. diff --git a/static/svrnty_nav.js b/static/svrnty_nav.js index bb229c7..090d322 100644 --- a/static/svrnty_nav.js +++ b/static/svrnty_nav.js @@ -22,19 +22,10 @@ '', bte: '', - graph: - '', }; 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); diff --git a/static/umbrella.css b/static/umbrella.css index 1afdd50..6463bfc 100644 --- a/static/umbrella.css +++ b/static/umbrella.css @@ -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; +} diff --git a/static/umbrella.html b/static/umbrella.html index dc3acf3..4991b33 100644 --- a/static/umbrella.html +++ b/static/umbrella.html @@ -7,6 +7,11 @@ +

Cortex-OS Umbrella

diff --git a/static/umbrella_inline.css b/static/umbrella_inline.css new file mode 100644 index 0000000..68e1131 --- /dev/null +++ b/static/umbrella_inline.css @@ -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; +} diff --git a/static/umbrella_inline.js b/static/umbrella_inline.js new file mode 100644 index 0000000..ea5f0c6 --- /dev/null +++ b/static/umbrella_inline.js @@ -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 = + ''; + + 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 = + ''; + 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(); + } +})(); diff --git a/tests/integration/test_loader_contract.py b/tests/integration/test_loader_contract.py index 649ea86..52b02fa 100644 --- a/tests/integration/test_loader_contract.py +++ b/tests/integration/test_loader_contract.py @@ -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 diff --git a/tests/unit/test_svrnty_nav_js.py b/tests/unit/test_svrnty_nav_js.py index 67237c7..384b895 100644 --- a/tests/unit/test_svrnty_nav_js.py +++ b/tests/unit/test_svrnty_nav_js.py @@ -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 diff --git a/tests/unit/test_umbrella_inline_static.py b/tests/unit/test_umbrella_inline_static.py new file mode 100644 index 0000000..251dba3 --- /dev/null +++ b/tests/unit/test_umbrella_inline_static.py @@ -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