mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-25 05:14:47 -06:00
fix(ui): re-home MCP consent badge on the Manage Connections row (#657)
* fix(ui): re-home MCP consent badge on the Manage Connections row The L-shell renovation retired the standalone settings gear (#settings-btn). The MCP pending-consent badge anchored to that gear via _refreshConsentBadge, which null-guarded silently — so since the renovation pending consent requests had no indicator (the badge was invisible). Re-home the badge on the rail's Manage row where the MCP/connections surface lives in both deployments: - rail.js gains a generic setRowBadge(tabKey, count, label?) hook + a `badge` builder: a small ⚠-glyph + count chip (never colour alone) using the DS warn tokens. mountManage registers row + owning-group-head refs and re-applies live counts across a (re)mount. When the owning group is collapsed, the count also mirrors onto the group head so a hidden row never hides the signal. rail.js stays agnostic — it owns the mechanism, the caller owns the meaning. - shell.js (the ESM bridge) re-exports setRowBadge on window.TS_SHELL so the classic ui/static/app.js subsystem can drive it without importing the module. - The standalone consent subsystem keeps its shell-level ownership: _refresh- ConsentBadge now drives setRowBadge on the Connections tab, fed by both the loadPendingConsents hydrate/poll load and live onConsentDetected notifications. - The shared interactive pane host bridges onConsentDetected to the new window.TS_APP.onConsentDetected seam (undefined on the console, so the console pane stays a no-op there); panes only notify. - The dead colour-only gear badge CSS (.settings-consent-badge, red dot) is removed; the new chip lives in shell.css as token-only .rail-badge so it flips themes by construction. Console MCP tab (Extensions > mcp) and standalone Connections tab (Extensions > connections) both badge correctly. Pins extended in test_shell_js.py + test_app_js.py. * fix(ui): drop the unused head ref from the rail badge row map Review feedback: _rowEls stored each row's group-head element but every head consumer resolves it through _groupEls; keeping the duplicate DOM ref made the remount state shape harder to reason about.
This commit is contained in:
+43
-7
@@ -428,9 +428,11 @@ def test_phase8_mcp_error_helpers_defined() -> None:
|
||||
(interactive consent / forbidden / operator card) moved into the shared
|
||||
interactive module with the Pane. The consent-badge state
|
||||
(``_pendingConsentServers`` / ``_onConsentDetected``) stays in the
|
||||
standalone shell — it drives the settings-gear badge — and the pane reaches
|
||||
it through the ``host.onConsentDetected`` seam (a no-op in the console,
|
||||
which has no gear badge). Pin both halves and the seam."""
|
||||
standalone shell — it drives the rail's Manage-row badge — and the pane
|
||||
reaches it through the ``host.onConsentDetected`` seam. The shared host
|
||||
bridges that seam to the standalone via ``window.TS_APP.onConsentDetected``
|
||||
(undefined on the console, so it stays a no-op there). Pin both halves and
|
||||
the bridge."""
|
||||
inter = _INTERACTIVE_JS.read_text(encoding="utf-8")
|
||||
assert "function tryParseMcpError" in inter
|
||||
assert "function buildMcpErrorEmbed" in inter
|
||||
@@ -440,14 +442,42 @@ def test_phase8_mcp_error_helpers_defined() -> None:
|
||||
assert "onConsentDetected(s)" in inter, (
|
||||
"the pane must notify consent through host.onConsentDetected"
|
||||
)
|
||||
# The shared host bridges the seam to the standalone subsystem (feature-
|
||||
# detected, so the console — which never defines the hook — no-ops).
|
||||
assert "window.TS_APP.onConsentDetected(server)" in inter, (
|
||||
"the shared interactive host must bridge onConsentDetected to the TS_APP seam"
|
||||
)
|
||||
app = _APP_JS.read_text(encoding="utf-8")
|
||||
assert "_pendingConsentServers" in app
|
||||
assert "function _onConsentDetected" in app
|
||||
assert "onConsentDetected(server)" in app, (
|
||||
"STANDALONE_HOST must wire host.onConsentDetected -> _onConsentDetected"
|
||||
assert "window.TS_APP.onConsentDetected = _onConsentDetected" in app, (
|
||||
"the standalone must expose _onConsentDetected on the TS_APP seam for the pane bridge"
|
||||
)
|
||||
|
||||
|
||||
def test_consent_badge_drives_rail_manage_row() -> None:
|
||||
"""The pending-consent badge was re-homed off the retired settings gear
|
||||
(``#settings-btn``, deleted in the L-shell renovation, which silently made
|
||||
the badge invisible) onto the rail's Manage > Connections row. Classic
|
||||
app.js can't import the ESM rail module, so it drives the rail's generic
|
||||
``setRowBadge`` hook through the ``window.TS_SHELL`` bridge — keyed on the
|
||||
standalone's Connections tab. Pin the new lane and the absence of the dead
|
||||
gear lookup."""
|
||||
app = _APP_JS.read_text(encoding="utf-8")
|
||||
# The badge refresh must drive the rail bridge, not the deleted gear.
|
||||
assert 'getElementById("settings-btn")' not in app, (
|
||||
"the consent badge must no longer target the retired #settings-btn gear"
|
||||
)
|
||||
assert "shell.setRowBadge(_CONSENT_BADGE_TAB" in app, (
|
||||
"_refreshConsentBadge must drive the rail Manage-row badge via the TS_SHELL bridge"
|
||||
)
|
||||
assert 'const _CONSENT_BADGE_TAB = "connections"' in app, (
|
||||
"the standalone badge rides the Connections Manage tab (its MCP surface)"
|
||||
)
|
||||
# The hydrate + clear paths must still funnel through the single refresh.
|
||||
assert "function loadPendingConsents" in app and "_refreshConsentBadge()" in app
|
||||
|
||||
|
||||
def test_media_player_activation_not_duplicated_in_standalone() -> None:
|
||||
"""The media-player activation (``_loadHls`` / ``_activatePlayer`` + the
|
||||
click/keydown delegate) moved into the shared interactive pane so BOTH the
|
||||
@@ -728,7 +758,9 @@ def test_phase8_css_classes_present_in_stylesheet() -> None:
|
||||
connections render in the Admin pane's Connections panel (#view-admin), not a
|
||||
floating dialog — so #settings-overlay / #settings-box are no longer pinned.
|
||||
The revoke confirm's chrome moved to /shared/hatch.css with the dialog-tier
|
||||
conversion, so no #revoke-mcp-* rule is pinned here either."""
|
||||
conversion, so no #revoke-mcp-* rule is pinned here either. The pending-
|
||||
consent badge moved off the retired settings gear onto the rail's Manage row
|
||||
(shell.css `.rail-badge`), so `.settings-consent-badge` is gone from here."""
|
||||
css = _STYLE_CSS.read_text(encoding="utf-8")
|
||||
for selector in [
|
||||
".mcp-error-card",
|
||||
@@ -736,9 +768,13 @@ def test_phase8_css_classes_present_in_stylesheet() -> None:
|
||||
".mcp-error-action-btn",
|
||||
".mcp-scope-pill",
|
||||
".settings-revoke-btn",
|
||||
".settings-consent-badge",
|
||||
]:
|
||||
assert selector in css, f"Missing CSS rule for {selector}"
|
||||
# The dead gear-badge rule must be GONE (its host #settings-btn was retired).
|
||||
assert ".settings-consent-badge" not in css, (
|
||||
"the retired settings-gear consent badge CSS must be removed "
|
||||
"(the badge now lives on the rail Manage row — shell.css .rail-badge)"
|
||||
)
|
||||
|
||||
|
||||
def test_phase8_consent_url_prefix_check_in_click_handler() -> None:
|
||||
|
||||
+59
-2
@@ -330,6 +330,63 @@ def test_step3_rail_manage_builds_from_admin_seam() -> None:
|
||||
assert "aria-expanded" in body, "collapsible group heads must expose aria-expanded"
|
||||
|
||||
|
||||
def test_rail_manage_row_badge_hook() -> None:
|
||||
"""rail.js owns a GENERIC Manage-row count badge — `setRowBadge(tabKey, count,
|
||||
label?)` stamps a glyph+count chip on a tab row (DS warn `.rail-badge`), and so
|
||||
a COLLAPSED group never hides the signal, mirrors the group's running total onto
|
||||
its head. rail.js stays agnostic about what the count means (no consent
|
||||
specifics here); a subsystem drives it. `mountManage` registers the row/head
|
||||
refs and re-applies live counts across a (re)mount. This is the re-homing
|
||||
target for the MCP consent badge after its settings-gear host was deleted."""
|
||||
body = _RAIL_JS.read_text(encoding="utf-8")
|
||||
assert "export function setRowBadge(tabKey, count, label)" in body, (
|
||||
"rail must export the generic setRowBadge hook (mechanism, not meaning)"
|
||||
)
|
||||
# The chip pairs colour with a glyph (never colour alone — chip-contrast rule).
|
||||
assert "rail-badge" in body and '"⚠"' in body, (
|
||||
"the badge must carry a ⚠ glyph alongside the count (not colour alone)"
|
||||
)
|
||||
# The collapsed-group head must carry the group total so a hidden row's signal
|
||||
# still surfaces — pin the head propagation + the per-group sum.
|
||||
assert "function _groupCount(" in body, "the head badge must sum the group's tab counts"
|
||||
assert "_groupEls" in body and "_rowEls" in body, (
|
||||
"mountManage must register row + owning-group-head refs for the badge hook"
|
||||
)
|
||||
assert "_reapplyBadges()" in body, (
|
||||
"a (re)mount must re-apply any live badge state (the refs are rebuilt)"
|
||||
)
|
||||
# rail.js stays agnostic — the hook takes a generic tabKey/count, with no
|
||||
# consent-specific endpoint, fetch, or branch (an explanatory comment naming a
|
||||
# sample caller is fine; logic is not). `setRowBadge` itself never fetches.
|
||||
badge_fn = body[body.index("export function setRowBadge") :]
|
||||
badge_fn = badge_fn[: badge_fn.index("\n}\n") + 3]
|
||||
assert "fetch" not in badge_fn and "/v1/" not in badge_fn, (
|
||||
"the generic badge hook must not reach into a subsystem (no fetch / endpoint)"
|
||||
)
|
||||
# No colour-only treatment: the chip uses DS warn tokens AND a glyph.
|
||||
css = _SHELL_CSS.read_text(encoding="utf-8")
|
||||
assert ".rail-badge" in css, "shell.css must carry the .rail-badge chip rule"
|
||||
badge_block = css[css.index(".rail-badge") :]
|
||||
badge_block = badge_block[: badge_block.index("\n.grp") if "\n.grp" in badge_block else 800]
|
||||
assert "var(--warn" in badge_block, (
|
||||
"the badge chip must use the DS --warn family (theme-flips by construction)"
|
||||
)
|
||||
assert "#" not in badge_block, "the badge chip must be token-only (no hex) so themes flip"
|
||||
|
||||
|
||||
def test_shell_bridges_setrowbadge_for_classic_subsystems() -> None:
|
||||
"""shell.js is the ESM module bridge: a classic-script subsystem (the
|
||||
standalone consent badge in ui/static/app.js) can't import rail.js, so the
|
||||
shell re-exports `setRowBadge` on the `window.TS_SHELL` seam. Pin the import
|
||||
and the seam so the bridge can't be silently dropped (which would re-break the
|
||||
badge the same way the gear deletion did)."""
|
||||
body = _SHELL_JS.read_text(encoding="utf-8")
|
||||
assert 'setRowBadge } from "./rail.js"' in body, "shell must import setRowBadge from rail.js"
|
||||
assert "notifySessionClosed, setRowBadge }" in body, (
|
||||
"TS_SHELL must expose setRowBadge for classic subsystems (the consent-badge bridge)"
|
||||
)
|
||||
|
||||
|
||||
def test_step3_admin_seam_and_thin_show_admin() -> None:
|
||||
"""admin.js exposes the TS_ADMIN seam (IA + shared perm gate + active-tab +
|
||||
openTab) and showAdmin is now a thin delegator that opens the singleton
|
||||
@@ -615,7 +672,7 @@ def test_step7_live_tab_state_glyphs() -> None:
|
||||
)
|
||||
assert "this.stateful" in pane, "ShellPane must carry the stateful flag"
|
||||
shell = _SHELL_JS.read_text(encoding="utf-8")
|
||||
assert 'import { mountRail, mountManage, glyph } from "./rail.js"' in shell, (
|
||||
assert 'import { mountRail, mountManage, glyph, setRowBadge } from "./rail.js"' in shell, (
|
||||
"the shell must import the rail's glyph builder (one source for tab + rail)"
|
||||
)
|
||||
assert "function stateForWs(" in shell and "function paintConvTabs(" in shell
|
||||
@@ -749,7 +806,7 @@ def test_shell_marks_pane_dead_on_ws_closed() -> None:
|
||||
assert "const notifySessionClosed = (wsId)" in shell
|
||||
assert 'pm.getPane("interactive", wsId)' in shell
|
||||
assert "p._ctl.markDead()" in shell
|
||||
assert "window.TS_SHELL = { panes: pm, caps, notifySessionClosed }" in shell, (
|
||||
assert "window.TS_SHELL = { panes: pm, caps, notifySessionClosed, setRowBadge }" in shell, (
|
||||
"the seam must be exported on TS_SHELL for the console's Tier-1 handler"
|
||||
)
|
||||
app = _CONSOLE_APP.read_text(encoding="utf-8")
|
||||
|
||||
@@ -3403,10 +3403,18 @@ function createInteractivePane(root, wsId, opts) {
|
||||
warningTarget(pane) {
|
||||
return pane.messagesEl;
|
||||
},
|
||||
// MCP re-consent surfaces inline in the pane card; the console has no
|
||||
// settings-gear badge to drive (a future console consent surface can hook
|
||||
// here).
|
||||
onConsentDetected() {},
|
||||
// MCP re-consent surfaces inline in the pane card; the STANDALONE additionally
|
||||
// drives a Manage-row attention badge — bridged through the TS_APP seam so the
|
||||
// shared factory stays deployment-agnostic (the console doesn't define the
|
||||
// hook, so this stays a no-op there).
|
||||
onConsentDetected(server) {
|
||||
if (
|
||||
window.TS_APP &&
|
||||
typeof window.TS_APP.onConsentDetected === "function"
|
||||
) {
|
||||
window.TS_APP.onConsentDetected(server);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const pane = new Pane(wsId, {
|
||||
|
||||
@@ -39,6 +39,24 @@ export function glyph(state) {
|
||||
return el;
|
||||
}
|
||||
|
||||
/** A small count chip for a Manage row/group head — glyph + count (never colour
|
||||
* alone), DS warn vocabulary (shell.css `.rail-badge`). The count rides the
|
||||
* text; the ⚠ glyph is aria-hidden because the supplied `label` already names
|
||||
* the condition for assistive tech. Returns a detached span the caller mounts. */
|
||||
function badge(count, label) {
|
||||
const el = document.createElement("span");
|
||||
el.className = "rail-badge";
|
||||
const g = document.createElement("span");
|
||||
g.className = "rail-badge-glyph";
|
||||
g.setAttribute("aria-hidden", "true");
|
||||
g.textContent = "⚠"; // ⚠ — pairs the colour with a glyph (chip-contrast rule)
|
||||
const n = document.createElement("b");
|
||||
n.textContent = String(count);
|
||||
el.append(g, n);
|
||||
if (label) el.setAttribute("aria-label", label);
|
||||
return el;
|
||||
}
|
||||
|
||||
/** Derive a node's overall state glyph from its workstream mix + health. */
|
||||
function nodeState(info) {
|
||||
if (!info.reachable) return "error";
|
||||
@@ -325,6 +343,80 @@ export function mountRail(sections, caps) {
|
||||
|
||||
// ---- Manage section (admin IA → collapsible discovery groups) --------------
|
||||
|
||||
// Generic Manage-row badge state. A subsystem (e.g. the standalone consent
|
||||
// badge) drives a count onto a tab row by KEY via `setRowBadge`; rail.js stays
|
||||
// agnostic about what the count means. Kept module-level so a `mountManage`
|
||||
// rebuild (the IA is static, but the section re-mounts on shell init) re-applies
|
||||
// the live counts rather than dropping them. `null` label = clear.
|
||||
const _rowBadges = new Map(); // tabKey -> { count, label }
|
||||
// Rebuilt each mount: the row <button> + owning group key for every tab. The
|
||||
// group's head <button> (where a collapsed group surfaces its summed badge —
|
||||
// a collapsed group hides its rows, so the head must carry the signal) is
|
||||
// looked up via _groupEls, not duplicated here.
|
||||
let _rowEls = new Map(); // tabKey -> { row, group }
|
||||
let _groupEls = new Map(); // group key -> { head, tabKeys: [] }
|
||||
|
||||
/** Paint (or clear) the badge slot inside a host button, keyed by a stable
|
||||
* `.rail-badge` child so repeated calls replace rather than stack. */
|
||||
function _applyBadge(host, count, label) {
|
||||
if (!host) return;
|
||||
const existing = host.querySelector(":scope > .rail-badge");
|
||||
if (!count) {
|
||||
if (existing) existing.remove();
|
||||
return;
|
||||
}
|
||||
const fresh = badge(count, label);
|
||||
if (existing) host.replaceChild(fresh, existing);
|
||||
else host.append(fresh);
|
||||
}
|
||||
|
||||
/** Sum the live badge counts for a group's tabs — drives the head badge so a
|
||||
* COLLAPSED group still shows that something inside it needs attention. */
|
||||
function _groupCount(groupKey) {
|
||||
const grp = _groupEls.get(groupKey);
|
||||
if (!grp) return 0;
|
||||
let total = 0;
|
||||
for (const tabKey of grp.tabKeys) {
|
||||
const b = _rowBadges.get(tabKey);
|
||||
if (b) total += b.count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic Manage-row badge hook. `setRowBadge(tabKey, count, label?)` stamps a
|
||||
* count chip on that tab's row and, so the signal survives a collapsed group,
|
||||
* mirrors the group's total onto its head. `count` 0 (or falsy) clears the row.
|
||||
* Drives off the refs `mountManage` registered; a no-op before the first mount
|
||||
* (the consent subsystem re-drives it after `_refreshConsentBadge`).
|
||||
*
|
||||
* rail.js owns the MECHANISM only — callers own the meaning (no consent specifics
|
||||
* here), matching the rail's seam-driven posture.
|
||||
*/
|
||||
export function setRowBadge(tabKey, count, label) {
|
||||
const n = Number(count) || 0;
|
||||
if (n > 0) _rowBadges.set(tabKey, { count: n, label: label || "" });
|
||||
else _rowBadges.delete(tabKey);
|
||||
const ref = _rowEls.get(tabKey);
|
||||
if (!ref) return; // not mounted yet (or gated away) — state is kept for remount
|
||||
_applyBadge(ref.row, n, label);
|
||||
// The collapsed-group head mirrors the group's running total + its own label.
|
||||
const total = _groupCount(ref.group);
|
||||
const grp = _groupEls.get(ref.group);
|
||||
_applyBadge(
|
||||
grp && grp.head,
|
||||
total,
|
||||
total ? total + " in " + ref.group + " awaiting attention" : "",
|
||||
);
|
||||
}
|
||||
|
||||
/** Re-stamp every stored badge after a (re)mount rebuilt the row/head refs.
|
||||
* `setRowBadge` self-guards a missing ref and recomputes each head total, so
|
||||
* replaying the stored rows lands both rows and heads at their correct sums. */
|
||||
function _reapplyBadges() {
|
||||
for (const [tabKey, b] of _rowBadges) setRowBadge(tabKey, b.count, b.label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the rail's Manage groups from the admin IA seam (admin.js exposes
|
||||
* `window.TS_ADMIN`). Each group is a collapsible `.grp` whose head toggles
|
||||
@@ -374,6 +466,9 @@ export function mountManage(root, paneManager) {
|
||||
const activeTab = adminOpen && TS.getActiveTab ? TS.getActiveTab() : null;
|
||||
|
||||
const rowByTab = new Map(); // tab -> its row <button>, for active-state sync
|
||||
// Rebuild the badge ref maps for this mount (the previous DOM is gone).
|
||||
_rowEls = new Map();
|
||||
_groupEls = new Map();
|
||||
|
||||
ia.forEach((group) => {
|
||||
const tabs = group.tabs.filter((t) => allowed(t.tab));
|
||||
@@ -404,6 +499,8 @@ export function mountManage(root, paneManager) {
|
||||
count.className = "gcount";
|
||||
count.textContent = String(tabs.length);
|
||||
head.append(chev, name, count);
|
||||
// Register the head so a collapsed group can carry its tabs' badge total.
|
||||
_groupEls.set(group.group, { head, tabKeys: tabs.map((t) => t.tab) });
|
||||
|
||||
const items = document.createElement("div");
|
||||
items.className = "grp-items";
|
||||
@@ -421,6 +518,9 @@ export function mountManage(root, paneManager) {
|
||||
if (TS.openTab) TS.openTab(t.tab);
|
||||
});
|
||||
rowByTab.set(t.tab, row);
|
||||
// Register the row + its owning group for the badge hook (the group's
|
||||
// head element is resolved through _groupEls when needed).
|
||||
_rowEls.set(t.tab, { row, group: group.group });
|
||||
items.append(row);
|
||||
}
|
||||
|
||||
@@ -434,6 +534,9 @@ export function mountManage(root, paneManager) {
|
||||
root.append(grp);
|
||||
});
|
||||
|
||||
// Re-apply any live row badges a subsystem set before/across this (re)mount.
|
||||
_reapplyBadges();
|
||||
|
||||
// Single writer for the Manage active-row: the row for the current admin tab
|
||||
// carries `.active`. admin.js notifies on every switchAdminTab; seed it here
|
||||
// for an already-open (restored) Admin pane.
|
||||
|
||||
@@ -997,6 +997,38 @@
|
||||
box-shadow: inset 2px 0 0 var(--ink-4);
|
||||
}
|
||||
|
||||
/* Manage-row attention badge (rail.js `setRowBadge` / `badge`) — a small count
|
||||
chip pinned to the row tail (and to a COLLAPSED group's head, so a hidden row
|
||||
never hides the signal). DS warn vocabulary: the ⚠ glyph PLUS the tinted
|
||||
chip carry the meaning (never colour alone), so it reads at chip size and
|
||||
flips themes by construction (the --warn family is defined per theme). */
|
||||
.rail-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-left: auto; /* push to the row/head tail past the flex-1 label */
|
||||
padding: 0 5px;
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--warn-tint);
|
||||
border: 1px solid var(--warn-tint-border);
|
||||
color: var(--warn);
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex: none;
|
||||
}
|
||||
/* The group head already auto-spaces with its own gap, and its --warn count
|
||||
should not fight the dim --ink-4 tab count beside it. */
|
||||
.grp-head .rail-badge {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.rail-badge .rail-badge-glyph {
|
||||
font-weight: 700;
|
||||
}
|
||||
.rail-badge b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== Admin pane — adopts #view-admin (the 18 tabpanels). The in-pane
|
||||
sidebar is retired (the rail's Manage groups navigate), so #view-admin fills
|
||||
the pane body and the content host renders full-width. ===== */
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
========================================================================== */
|
||||
|
||||
import { PaneManager, ShellPane, openPopupMenu } from "./pane.js";
|
||||
import { mountRail, mountManage, glyph } from "./rail.js";
|
||||
import { mountRail, mountManage, glyph, setRowBadge } from "./rail.js";
|
||||
import { authFetch } from "./auth.js";
|
||||
// The interactive pane is a real ES module beside us in /shared (step 5a) — the
|
||||
// shell imports it directly, and it exists in every deployment. The coordinator
|
||||
@@ -880,7 +880,11 @@ async function mountShell() {
|
||||
const p = pm.getPane("interactive", wsId);
|
||||
if (p && p._ctl && p._ctl.markDead) p._ctl.markDead();
|
||||
};
|
||||
window.TS_SHELL = { panes: pm, caps, notifySessionClosed };
|
||||
// `setRowBadge` lets a classic-script subsystem (the standalone consent badge
|
||||
// in ui/static/app.js) stamp a count chip on a Manage row without importing the
|
||||
// ESM rail module — the shell is its module bridge. Generic: the rail owns the
|
||||
// chip mechanism, the caller owns what the count means.
|
||||
window.TS_SHELL = { panes: pm, caps, notifySessionClosed, setRowBadge };
|
||||
|
||||
// Login fan-out: app.js owns the single window.onLoginSuccess (the Tier-1
|
||||
// reconnect, set at load). Wrap it in a tiny registry so EVERY conversational
|
||||
|
||||
+29
-28
@@ -1473,17 +1473,19 @@ function connectGlobalSSE() {
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 12. MCP consent badge (standalone settings-gear pending-consent indicator)
|
||||
// 12. MCP consent badge (standalone pending-consent indicator)
|
||||
//
|
||||
// The tool-output / media / MCP-error / verdict renderers that used to live in
|
||||
// this section moved to shared_static/interactive.js with the Pane. What
|
||||
// stays here is the standalone consent-badge subsystem: the gear badge lives
|
||||
// in this shell's header, so the pane only notifies it (host.onConsentDetected
|
||||
// -> _onConsentDetected) and the dashboard hydrates it via loadPendingConsents.
|
||||
// stays here is the standalone consent-badge subsystem: it owns the pending set
|
||||
// and drives the rail's Manage > Connections row badge (via the TS_SHELL bridge
|
||||
// — `setRowBadge`). An interactive pane only NOTIFIES it (the shared host
|
||||
// bridges `onConsentDetected` to the TS_APP seam below); `loadPendingConsents`
|
||||
// hydrates it on boot. The settings-gear it used to hang on is retired.
|
||||
// ===========================================================================
|
||||
|
||||
// Module-level set of servers with an unresolved consent prompt; drives the
|
||||
// gear-icon badge so the user has a stable signal that re-consent is pending
|
||||
// Manage-row badge so the user has a stable signal that re-consent is pending
|
||||
// after the inline card scrolls out of view.
|
||||
const _pendingConsentServers = new Set();
|
||||
|
||||
@@ -1528,32 +1530,26 @@ function loadPendingConsents() {
|
||||
});
|
||||
}
|
||||
|
||||
// The Manage tab the pending-consent badge rides on. The standalone's Manage IA
|
||||
// (TS_ADMIN.ia, below) is a single Extensions > Connections tab where MCP server
|
||||
// connections live; the badge surfaces there (and, when that group is collapsed,
|
||||
// on its head — the rail handles that). The retired settings-gear it used to
|
||||
// hang on is gone with the L-shell renovation.
|
||||
const _CONSENT_BADGE_TAB = "connections";
|
||||
|
||||
function _refreshConsentBadge() {
|
||||
const btn = document.getElementById("settings-btn");
|
||||
if (!btn) return;
|
||||
let existing = btn.querySelector(".settings-consent-badge");
|
||||
const n = _pendingConsentServers.size;
|
||||
// Keep the visible badge and the accessible name in lockstep so screen-
|
||||
// reader users get the same pending-consent signal that sighted users
|
||||
// get from the red dot. The badge itself stays aria-hidden because the
|
||||
// count is already reflected in the button's aria-label/title.
|
||||
if (n === 0) {
|
||||
if (existing) existing.remove();
|
||||
btn.setAttribute("aria-label", "Settings");
|
||||
btn.setAttribute("title", "Settings");
|
||||
return;
|
||||
}
|
||||
if (!existing) {
|
||||
existing = document.createElement("span");
|
||||
existing.className = "settings-consent-badge";
|
||||
existing.setAttribute("aria-hidden", "true");
|
||||
btn.appendChild(existing);
|
||||
}
|
||||
existing.textContent = String(n);
|
||||
// Drive the rail's generic Manage-row badge through the shell bridge (classic
|
||||
// app.js can't import the ESM rail module). The chip's own ⚠ glyph + count
|
||||
// carry the signal; `label` keeps the accessible name in lockstep. A no-op
|
||||
// before the rail mounts — `loadPendingConsents` re-drives it after boot.
|
||||
const shell = window.TS_SHELL;
|
||||
if (!shell || typeof shell.setRowBadge !== "function") return;
|
||||
const label =
|
||||
"Settings (" + n + " MCP consent" + (n === 1 ? "" : "s") + " pending)";
|
||||
btn.setAttribute("aria-label", label);
|
||||
btn.setAttribute("title", label);
|
||||
n === 0
|
||||
? ""
|
||||
: n + " MCP server" + (n === 1 ? "" : "s") + " awaiting consent";
|
||||
shell.setRowBadge(_CONSENT_BADGE_TAB, n, label);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2230,6 +2226,11 @@ window.TS_APP.onRender = function (cb) {
|
||||
};
|
||||
window.TS_APP.bucketByParent = bucketByParent;
|
||||
window.TS_APP.boot = boot;
|
||||
// Live MCP-consent notifications from an interactive pane (the shared pane host
|
||||
// bridges its `onConsentDetected` here when this seam exists; the console leaves
|
||||
// it undefined, so the pane no-ops there). Adds the server to the pending set
|
||||
// and re-paints the Manage-row badge.
|
||||
window.TS_APP.onConsentDetected = _onConsentDetected;
|
||||
|
||||
// --- Manage seam: one Connections tab (MCP server connections) -------------
|
||||
const _CONN_IA = [
|
||||
|
||||
@@ -1822,16 +1822,3 @@ audio.media-player {
|
||||
.settings-revoke-btn:hover {
|
||||
background: rgba(255, 0, 0, 0.05);
|
||||
}
|
||||
.settings-consent-badge {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
background: var(--red);
|
||||
color: var(--bg);
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
vertical-align: top;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user