Files
fusion/packages/dashboard/app/components/RuntimeFallbackBadge.tsx
Fusion 0bed997af8 feat: surface runtime-resolution fallback in dashboard, thread real FallbackReason
Fixes silent runtime fallback visibility (dashboard never read wasConfigured
or session:runtime-resolved) and threads the real FallbackReason
(not_found vs factory_error) through resolveRuntime()/logRuntimeFallback
instead of hardcoding "not_found" for every fallback.

- packages/engine/src/runtime-resolution.ts: resolvePluginRuntime() now
  returns a tagged miss result distinguishing not_found from factory_error;
  resolveRuntime() threads the real reason through and returns it as
  ResolvedRuntime.fallbackReason
- packages/engine/src/agent-session-helpers.ts: includes fallbackReason in
  the session:runtime-resolved audit event metadata
- packages/dashboard/src/routes/register-task-workflow-routes.ts: new
  GET /api/tasks/:id/runtime-fallback endpoint
- packages/dashboard/app/hooks/useRuntimeFallbackStatus.ts +
  packages/dashboard/app/components/RuntimeFallbackBadge.tsx: new polling
  hook + badge/toast component wired into TaskCard, ActiveAgentsPanel, and
  AgentsView

Ref: Fusion task FUX-022, investigations/FUX-017-hermes-runtime-fallback.md
recommendation #1
2026-07-08 03:09:29 -04:00

58 lines
2.2 KiB
TypeScript

/**
* RuntimeFallbackBadge (FUX-022)
*
* Renders a visible badge on an agent/task card when the most recent
* `session:runtime-resolved` audit event for that task shows
* `wasConfigured: false` alongside a non-empty configured `runtimeHint` —
* i.e. the configured runtime (e.g. "hermes") could not be resolved and the
* session silently fell back to the default `pi` runtime. Also fires a toast
* via the shared ToastProvider the first time this fallback state is newly
* observed for a session (not re-fired on every poll/re-render).
*
* Renders null (no leftover placeholder) for every other state: no event
* yet, wasConfigured true, or wasConfigured false with a blank hint.
*/
import { memo, useEffect } from "react";
import { AlertTriangle } from "lucide-react";
import { useRuntimeFallbackStatus } from "../hooks/useRuntimeFallbackStatus";
import { useToast } from "../hooks/useToast";
interface RuntimeFallbackBadgeProps {
taskId?: string;
/** Gate polling to visible cards only (e.g. pass the card's own isInViewport state). */
isInViewport: boolean;
projectId?: string;
}
function RuntimeFallbackBadgeComponent({ taskId, isInViewport, projectId }: RuntimeFallbackBadgeProps) {
const { addToast } = useToast();
const status = useRuntimeFallbackStatus(taskId, isInViewport, projectId);
useEffect(() => {
if (status.shouldToastNow && status.message) {
addToast(status.message, "warning");
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- addToast identity is stable per ToastProvider instance
}, [status.shouldToastNow, status.message]);
if (!status.showBadge || !status.message) {
return null;
}
return (
<span
className="card-status-badge card-runtime-fallback-badge"
title={status.message}
data-testid="runtime-fallback-badge"
data-runtime-hint={status.runtimeHint ?? undefined}
data-runtime-fallback-reason={status.reason ?? undefined}
>
<AlertTriangle size={10} aria-hidden="true" />
<span>{status.message}</span>
</span>
);
}
export const RuntimeFallbackBadge = memo(RuntimeFallbackBadgeComponent);
RuntimeFallbackBadge.displayName = "RuntimeFallbackBadge";