mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 } = {},
|
||||
|
||||
Reference in New Issue
Block a user