feat(agents): per-runtime install/start controls via RuntimesSection

The agents page only surfaced OpenClaw's lifecycle controls — Hermes
auto-installed silently at boot with no UI visibility or manual handle.
Adds a generic section that iterates over container-kind runtimes from
/runtimes and renders a control panel + status bar per adapter.

- new useRuntimes() hook hits GET /runtimes
- new RuntimesSection renders one card per container runtime, with an
  adapter-keyed extras registry for adapter-specific affordances
  (panel extras + status-bar pill / actions)
- AgentsPage replaces its hand-rolled openclaw panel + bar with the
  section, plugging Configure-provider + Terminal into the openclaw
  slot via the registry
- the section becomes adapter-agnostic: new container runtimes show up
  on the page automatically (filtered by descriptor.kind === 'container')
This commit is contained in:
DaniAkash
2026-05-11 20:15:00 +05:30
parent 9632b60425
commit b6172a4109
3 changed files with 115 additions and 38 deletions

View File

@@ -28,8 +28,7 @@ import {
} from './agents-page-utils'
import { NewAgentDialog } from './NewAgentDialog'
import { InlineErrorAlert } from './OpenClawControls'
import { RuntimeControlPanel } from './runtime-controls/RuntimeControlPanel'
import { RuntimeStatusBar } from './runtime-controls/RuntimeStatusBar'
import { RuntimesSection } from './runtime-controls/RuntimesSection'
import { SetupOpenClawDialog } from './SetupOpenClawDialog'
import {
useAgentAdapters,
@@ -261,13 +260,6 @@ export const AgentsPage: FC = () => {
)
}
// Bar only makes sense when the gateway is running AND there's at
// least one OpenClaw agent in the merged list. Hide it for
// Claude/Codex-only setups so the page stays uncluttered.
const showGatewayStatusBar =
openClawRunning &&
(visibleOpenClawAgents.length > 0 ||
harnessAgents.some((agent) => agent.adapter === 'openclaw'))
// Setup CTA appears when the runtime is healthy but the user has not
// yet configured a provider (no openclaw.json on disk → runtime is
// running but agent CRUD will fail). For now: surface it whenever the
@@ -287,37 +279,32 @@ export const AgentsPage: FC = () => {
/>
) : null}
<RuntimeControlPanel
adapter="openclaw"
extras={
showSetupCta ? (
<Button
size="sm"
variant="outline"
onClick={() => setSetupOpen(true)}
>
Configure provider
</Button>
) : null
}
<RuntimesSection
extras={{
openclaw: {
panelExtras: showSetupCta ? (
<Button
size="sm"
variant="outline"
onClick={() => setSetupOpen(true)}
>
Configure provider
</Button>
) : null,
statusBarExtraActions: (
<Button
variant="ghost"
size="sm"
onClick={() => setShowTerminal(true)}
>
<TerminalIcon className="mr-1.5 h-3.5 w-3.5" />
Terminal
</Button>
),
},
}}
/>
{showGatewayStatusBar ? (
<RuntimeStatusBar
adapter="openclaw"
extraActions={
<Button
variant="ghost"
size="sm"
onClick={() => setShowTerminal(true)}
>
<TerminalIcon className="mr-1.5 h-3.5 w-3.5" />
Terminal
</Button>
}
/>
) : null}
<AgentList
agents={agentListItems}
activity={agentActivity}

View File

@@ -0,0 +1,72 @@
import type { FC, ReactNode } from 'react'
import {
type RuntimeAdapterId,
type RuntimeView,
useRuntimes,
} from '../useRuntime'
import { RuntimeControlPanel } from './RuntimeControlPanel'
import { RuntimeStatusBar } from './RuntimeStatusBar'
/** Optional adapter-specific UI hooks. Each runtime can plug in extras
* for the control panel (e.g. openclaw's "Configure provider…") and
* the status bar (extraPill, extraActions). Missing keys fall back to
* the generic panel/bar with no extras. */
export interface RuntimeAdapterExtras {
panelExtras?: ReactNode
statusBarExtraPill?: ReactNode
statusBarExtraActions?: ReactNode
}
interface RuntimesSectionProps {
/** Per-adapter customization keyed by adapterId. Adapters not in the
* map render the generic UI. */
extras?: Partial<Record<RuntimeAdapterId, RuntimeAdapterExtras>>
}
/** Renders one card per container-kind runtime (openclaw, hermes, …)
* with state-appropriate Install / Start / Restart controls and a
* status bar. Adapter-specific affordances slot in via `extras`. */
export const RuntimesSection: FC<RuntimesSectionProps> = ({ extras }) => {
const { data, isLoading } = useRuntimes()
if (isLoading || !data) return null
const containerRuntimes = data.filter(
(r) => r.descriptor.kind === 'container',
)
if (containerRuntimes.length === 0) return null
return (
<div className="flex flex-col gap-3">
{containerRuntimes.map((runtime) => (
<RuntimeCard
key={runtime.descriptor.adapterId}
runtime={runtime}
extras={extras?.[runtime.descriptor.adapterId as RuntimeAdapterId]}
/>
))}
</div>
)
}
interface RuntimeCardProps {
runtime: RuntimeView
extras?: RuntimeAdapterExtras
}
const RuntimeCard: FC<RuntimeCardProps> = ({ runtime, extras }) => {
const adapter = runtime.descriptor.adapterId as RuntimeAdapterId
const showStatusBar = runtime.status.state === 'running'
return (
<div className="flex flex-col gap-3">
<RuntimeControlPanel adapter={adapter} extras={extras?.panelExtras} />
{showStatusBar && (
<RuntimeStatusBar
adapter={adapter}
extraPill={extras?.statusBarExtraPill}
extraActions={extras?.statusBarExtraActions}
/>
)}
</div>
)
}

View File

@@ -56,6 +56,24 @@ export const RUNTIME_QUERY_KEYS = {
logs: (adapter: RuntimeAdapterId) => ['runtime-logs', adapter] as const,
} as const
export function useRuntimes(opts: { pollMs?: number } = {}) {
const rpcClient = useRpcClient()
return useQuery<RuntimeView[], Error>({
queryKey: [RUNTIME_QUERY_KEYS.list],
queryFn: async () => {
const res = await rpcClient.runtimes.$get()
if (!res.ok) {
const body = (await res.json()) as { error?: string }
throw new Error(body.error ?? 'runtimes list fetch failed')
}
const { runtimes } = (await res.json()) as { runtimes: RuntimeView[] }
return runtimes
},
refetchInterval: opts.pollMs ?? 5_000,
retry: false,
})
}
export function useRuntime(
adapter: RuntimeAdapterId,
opts: { pollMs?: number; enabled?: boolean } = {},