Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
carry ~5,930 keys each across common/app/errors/cli namespaces;
CLI bundles regenerated (6 locales incl. ko)
Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
literal {{placeholders}}); dashboard vitest.setup boots a minimal en
i18next instance for the same reason
Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
/**
|
|
* Shared visual shell for the three runtime-provider settings cards
|
|
* (Hermes, OpenClaw, Paperclip).
|
|
*
|
|
* Renders a consistent header (logo + name + status badge + Learn more link),
|
|
* a description block, a slot for connection-mode tabs, the form contents the
|
|
* caller passes as children, and a footer with Test/Save/Save & Test buttons
|
|
* plus a status toast that surfaces probe + save outcomes.
|
|
*/
|
|
|
|
import { ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
export type RuntimeStatusKind = "neutral" | "ok" | "err" | "loading";
|
|
|
|
export interface RuntimeCardShellProps {
|
|
/** Name shown next to the logo (e.g. "Hermes"). */
|
|
name: string;
|
|
/** Optional sub-line under the name (e.g. "by Nous Research"). */
|
|
subname?: string;
|
|
/** The provider logo. Use a `<ProviderIcon>` or an inline `<img>` for brand SVGs. */
|
|
logo: ReactNode;
|
|
/** "Learn more" target — official documentation/site. */
|
|
learnMoreHref: string;
|
|
/** Status kind drives the badge color. */
|
|
statusKind: RuntimeStatusKind;
|
|
/** Status text (e.g. "✓ Detected v0.8.0"). */
|
|
statusText: string;
|
|
/** Description shown under the header. */
|
|
description: ReactNode;
|
|
/** Optional extra header chrome (e.g. tabs). Rendered above the form. */
|
|
tabs?: ReactNode;
|
|
/** The form fields. */
|
|
children: ReactNode;
|
|
/** Disabled state for the action row. */
|
|
busy: "loading" | "saving" | "testing" | "save-test" | null;
|
|
/** Disable Test if some required input is missing. */
|
|
canTest?: boolean;
|
|
/** Toast under the action row — shown when not null. */
|
|
toast?: { kind: "ok" | "err"; message: string } | null;
|
|
onTest: () => void;
|
|
onSave: () => void;
|
|
onSaveAndTest: () => void;
|
|
/** Optional extra content right above the footer (e.g. an install hint). */
|
|
belowForm?: ReactNode;
|
|
/** A test-id forwarded to the root element. */
|
|
testId?: string;
|
|
}
|
|
|
|
export function RuntimeCardShell(props: RuntimeCardShellProps) {
|
|
const { t } = useTranslation("app");
|
|
const {
|
|
name,
|
|
subname,
|
|
logo,
|
|
learnMoreHref,
|
|
statusKind,
|
|
statusText,
|
|
description,
|
|
tabs,
|
|
children,
|
|
busy,
|
|
canTest = true,
|
|
toast,
|
|
onTest,
|
|
onSave,
|
|
onSaveAndTest,
|
|
belowForm,
|
|
testId,
|
|
} = props;
|
|
|
|
const statusClass =
|
|
statusKind === "ok"
|
|
? "runtime-card__status runtime-card__status--ok"
|
|
: statusKind === "err"
|
|
? "runtime-card__status runtime-card__status--err"
|
|
: "runtime-card__status runtime-card__status--neutral";
|
|
|
|
return (
|
|
<div className="runtime-card" data-testid={testId} aria-live="polite">
|
|
<header className="runtime-card__header">
|
|
<span className="runtime-card__logo">{logo}</span>
|
|
<div className="runtime-card__title">
|
|
<h3 className="runtime-card__name">{name}</h3>
|
|
{subname && (
|
|
<small className="runtime-card__cobrand">{subname}</small>
|
|
)}
|
|
<small className={statusClass}>
|
|
{statusKind === "loading" && <Loader2 size={12} className="animate-spin" />}
|
|
{statusText}
|
|
</small>
|
|
</div>
|
|
<a
|
|
className="runtime-card__learn-more btn btn-sm btn-ghost"
|
|
href={learnMoreHref}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
{t("common.learnMore", "Learn more →")}
|
|
</a>
|
|
</header>
|
|
|
|
<p className="runtime-card__description">{description}</p>
|
|
|
|
{tabs}
|
|
|
|
<div className="runtime-card__form">{children}</div>
|
|
|
|
{belowForm}
|
|
|
|
<footer className="runtime-card__footer">
|
|
{toast && (
|
|
<span
|
|
className={
|
|
toast.kind === "ok"
|
|
? "runtime-card__toast runtime-card__toast--ok"
|
|
: "runtime-card__toast runtime-card__toast--err"
|
|
}
|
|
role="status"
|
|
>
|
|
{toast.message}
|
|
</span>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm"
|
|
onClick={onTest}
|
|
disabled={busy !== null || !canTest}
|
|
>
|
|
{busy === "testing" ? (
|
|
<>
|
|
<Loader2 size={12} className="animate-spin" /> {t("common.testing", "Testing…")}
|
|
</>
|
|
) : (
|
|
t("common.test", "Test")
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm"
|
|
onClick={onSave}
|
|
disabled={busy !== null}
|
|
>
|
|
{busy === "saving" ? t("common.saving", "Saving…") : t("common.save", "Save")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary btn-sm"
|
|
onClick={onSaveAndTest}
|
|
disabled={busy !== null || !canTest}
|
|
>
|
|
{busy === "save-test" ? (
|
|
<>
|
|
<Loader2 size={12} className="animate-spin" /> {t("common.saving", "Saving…")}
|
|
</>
|
|
) : (
|
|
t("common.saveAndTest", "Save & Test")
|
|
)}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|