From 6f1072c83cc33411fec1b5a276357ab88332abc6 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 6 Aug 2026 22:29:33 -0500 Subject: [PATCH] fix(desktop): drop the gateway-pill dogfood plugin It was a 1:1 rebuild of the core statusbar gateway item and shipped enabled by default, so the pill showed up twice. Core chrome stays in shell; demos that clone it belong in hermes-example-plugins. --- apps/desktop/src/plugins/README.md | 8 +- .../src/plugins/gateway-pill/plugin.tsx | 375 ------------------ 2 files changed, 5 insertions(+), 378 deletions(-) delete mode 100644 apps/desktop/src/plugins/gateway-pill/plugin.tsx diff --git a/apps/desktop/src/plugins/README.md b/apps/desktop/src/plugins/README.md index adcad66e23077..10ec027a9aba4 100644 --- a/apps/desktop/src/plugins/README.md +++ b/apps/desktop/src/plugins/README.md @@ -4,10 +4,12 @@ Drop a `/plugin.{ts,tsx}` here that default-exports a `HermesPlugin` and it registers automatically at boot (vite glob in `../contrib/plugins.ts`), with the same inventory + live enable/disable contract as runtime plugins. -None ship in-tree today — reference/demo plugins (the counter example, the -gateway-pill 1:1 rebuild, the runtime-loader hello world) live in the companion +Keep this tree for real shipped plugins (and the small authoring fixtures that +dogfood the SDK). One-off demos that rebuild a core chrome piece 1:1 do not +belong here — they double the UI and confuse Settings ▸ Plugins. Publish those +in the companion [`hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins) -repo so the shipped app stays uncluttered. +repo instead. User- and agent-authored plugins load at runtime from `$HERMES_HOME/desktop-plugins//plugin.js` (the disk door) — see the diff --git a/apps/desktop/src/plugins/gateway-pill/plugin.tsx b/apps/desktop/src/plugins/gateway-pill/plugin.tsx deleted file mode 100644 index 60064f1c53b28..0000000000000 --- a/apps/desktop/src/plugins/gateway-pill/plugin.tsx +++ /dev/null @@ -1,375 +0,0 @@ -/** - * Gateway pill — the core statusbar gateway-health item implemented 1:1 as a - * plugin: same trigger chrome (declarative `variant: 'menu'` StatusbarItem → - * the app's own portal/popover plumbing, so nothing clips), same menu panel - * (connection/inference rows, restart, reason, RECENT ACTIVITY tail, - * messaging platforms), same copy (`useI18n`), same readiness logic - * (`evaluateRuntimeReadiness` over `host.request`). The point: a plugin can - * rebuild a REAL core feature through the SDK alone — only - * `@hermes/plugin-sdk` + react (lint-fenced). - * - * Pattern notes: - * - a module-level `atom` shares the readiness poll between the live label - * elements and the menu panel (the same primitive `host.state` uses); - * - label/detail/icon of a DATA item are ReactNodes, so they can be tiny - * components that subscribe — a static item shape with live innards. - */ - -import { - atom, - Button, - cn, - evaluateRuntimeReadiness, - type HermesPlugin, - host, - icons, - LogView, - type RuntimeReadinessResult, - type StatusbarItem, - StatusDot, - type StatusResponse, - type StatusTone, - Tip, - useI18n, - useValue -} from '@hermes/plugin-sdk' -import { type ReactNode, useEffect, useRef, useState } from 'react' - -const READINESS_POLL_MS = 15_000 -const LOG_TAIL = 120 -const LOG_VISIBLE = 40 -const LOG_POLL_MS = 3_000 - -// Per-connection WebSocket churn (accept/close/heartbeat) drowns out anything -// useful — strip it so the tail reads as real gateway activity at a glance. -const LOG_NOISE_RE = /\bws (?:accepted|closed|response sent|ping|pong)\b/i - -// Strip leading "YYYY-MM-DD HH:MM:SS,mmm " and "[runtime_id] " prefixes. -const TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}[,.\d]*\s+/ -const RUNTIME_BRACKET_RE = /^\[[^\]]+]\s+/ -const trimLogLine = (raw: string) => raw.trim().replace(TIMESTAMP_RE, '').replace(RUNTIME_BRACKET_RE, '') - -const PLATFORM_TONE: Record = { - connected: 'good', - connecting: 'warn', - retrying: 'warn', - pending_restart: 'warn', - startup_failed: 'bad', - fatal: 'bad' -} - -const prettyState = (state: string) => state.replace(/_/g, ' ').replace(/^./, c => c.toUpperCase()) - -const SYSTEM_PANEL_ROUTE = '/command-center?section=system' - -// --------------------------------------------------------------------------- -// Readiness poll — one loop at plugin scope, shared by label + panel. -// --------------------------------------------------------------------------- - -const $readiness = atom(null) - -function startReadinessPoll() { - let timer: null | number = null - - const stop = () => { - if (timer !== null) { - window.clearInterval(timer) - timer = null - } - - $readiness.set(null) - } - - const refresh = () => - evaluateRuntimeReadiness(host.request) - .then(next => $readiness.set(next)) - .catch(() => undefined) - - const sync = (gateway: string) => { - if (gateway !== 'open') { - stop() - - return - } - - if (timer === null) { - void refresh() - timer = window.setInterval(() => void refresh(), READINESS_POLL_MS) - } - } - - sync(host.state.gateway.get()) - host.state.gateway.listen(sync) -} - -// --------------------------------------------------------------------------- -// Live trigger innards (the item is static DATA; these subscribe). -// --------------------------------------------------------------------------- - -function useHealth() { - const gateway = useValue(host.state.gateway) - const readiness = useValue($readiness) - - return { - connecting: gateway === 'connecting', - open: gateway === 'open', - readiness, - ready: gateway === 'open' && readiness?.ready === true - } -} - -function PillIcon() { - const { connecting, open, ready } = useHealth() - - return ( - - {ready ? : } - - ) -} - -function PillDetail() { - const { t } = useI18n() - const copy = t.shell.statusbar - const { connecting, open, readiness, ready } = useHealth() - - const detail = open - ? ready - ? copy.gatewayReady - : readiness - ? copy.gatewayNeedsSetup - : copy.gatewayChecking - : connecting - ? copy.gatewayConnecting - : copy.gatewayOffline - - return <>{detail} -} - -// --------------------------------------------------------------------------- -// The menu panel — the real GatewayMenuPanel, rebuilt on SDK doors. -// --------------------------------------------------------------------------- - -/** Live gui-log tail while the popover is mounted (i.e. open). */ -function useGatewayLogTail(): string[] { - const [lines, setLines] = useState([]) - - useEffect(() => { - let cancelled = false - - const load = () => - host - .logs({ file: 'gui', lines: LOG_TAIL }) - .then(res => { - if (!cancelled) { - setLines( - res.lines - .map(line => line.trim()) - .filter(line => line && !LOG_NOISE_RE.test(line)) - .slice(-LOG_VISIBLE) - ) - } - }) - .catch(() => undefined) - - void load() - const timer = window.setInterval(load, LOG_POLL_MS) - - return () => { - cancelled = true - window.clearInterval(timer) - } - }, []) - - return lines -} - -function Section({ children, className }: { children: ReactNode; className?: string }) { - return
{children}
-} - -function SectionLabel({ children }: { children: string }) { - return ( -
{children}
- ) -} - -function GatewayMenuPanel({ onClose }: { onClose: () => void }) { - const { t } = useI18n() - const copy = t.shell.gatewayMenu - const gateway = useValue(host.state.gateway) - const { readiness, ready } = useHealth() - const [snapshot, setSnapshot] = useState(null) - const recentLogs = useGatewayLogTail() - - useEffect(() => { - void host - .status() - .then(setSnapshot) - .catch(() => undefined) - }, []) - - const openSystem = () => { - onClose() - host.navigate(SYSTEM_PANEL_ROUTE) - } - - const restart = () => { - onClose() - void host.restartGateway().catch(() => undefined) - } - - const gatewayOpen = gateway === 'open' - const gatewayConnecting = gateway === 'connecting' - - const connectionLabel = gatewayOpen - ? copy.connected - : gatewayConnecting - ? copy.connecting - : prettyState(gateway || copy.offline) - - const inferenceLabel = gatewayOpen - ? readiness?.ready - ? copy.inferenceReady - : readiness - ? copy.inferenceNotReady - : copy.checkingInference - : copy.disconnected - - const platforms = Object.entries(snapshot?.gateway_platforms || {}).sort(([l], [r]) => l.localeCompare(r)) - - // Keep the tail pinned to the latest line as it streams. - const logScrollRef = useRef(null) - - useEffect(() => { - const el = logScrollRef.current - - if (el) { - el.scrollTop = el.scrollHeight - } - }, [recentLogs]) - - return ( -
-
-
- - - {connectionLabel} - - - - {inferenceLabel} - -
-
- - - - - - -
-
- - {readiness?.reason && ( -
-
{readiness.reason}
-
- )} - - {recentLogs.length > 0 && ( -
-
- {copy.recentActivity} - -
- - {recentLogs.map(trimLogLine).join('\n')} - -
- )} - - {platforms.length > 0 && ( -
- {copy.messagingPlatforms} -
    - {platforms.map(([name, platform]) => ( -
  • - {name} - - - {prettyState(platform.state)} - -
  • - ))} -
-
- )} -
- ) -} - -function PillLabel() { - const { t } = useI18n() - - return <>{t.shell.statusbar.gateway} -} - -// --------------------------------------------------------------------------- - -const plugin: HermesPlugin = { - id: 'gateway-pill', - name: 'Gateway Pill', - register(ctx) { - startReadinessPoll() - - // Declarative menu item — the app's own trigger/popover chrome renders it - // (portal, w-72, side=top), the plugin supplies live innards + the panel. - ctx.register({ - id: 'pill', - area: 'statusBar.right', - order: 90, - data: { - icon: , - id: 'gateway-pill', - label: , - detail: , - menuClassName: 'w-72', - menuContent: (close: () => void) => , - variant: 'menu' - } satisfies StatusbarItem - }) - } -} - -export default plugin