mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat(web): surface the specific Box failure reason in unavailable banner
When Box is configured but the runtime reports its backend is dead
(e.g. ``box.backend = nsjail`` but the binary is missing, or Docker
daemon crashed), the backend now returns a structured
``connector_error`` like ``Configured sandbox backend "nsjail" is
unavailable``. The previous notice only said "Box sandbox is
unavailable" + a generic "enable Box" hint, hiding the actionable
detail.
- ``useBoxStatus``: derive ``reason`` from ``status.connector_error``.
Only exposed for the failed-state (``hint === 'boxUnavailable'``),
since the disabled-by-config message already carries its reason
- ``BoxUnavailableNotice``: insert the reason as a small monospaced
line between the state message and the action hint. The disabled
variant is unchanged (operator chose the state)
- Wire ``reason`` through every existing call site (Skills page +
detail, PipelineExtension, both MCP forms). Old unused ``context``
prop dropped
Net layout (3 lines, still compact):
⚠ Box sandbox is unavailable — sandbox tools, skill add/edit, ...
Configured sandbox backend "nsjail" is unavailable
This feature requires the Box runtime. Enable it in config ...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,16 +13,18 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
*/
|
||||
export interface BoxUnavailableNoticeProps {
|
||||
hint: 'boxDisabled' | 'boxUnavailable' | null;
|
||||
/** Optional extra context line (e.g. the specific consumer name). */
|
||||
context?: string;
|
||||
/** When true, render as muted; default uses the destructive variant only
|
||||
* for failed (boxUnavailable) state so a deliberate disable looks calm. */
|
||||
/** Specific failure reason from the backend (``connector_error``). Shown
|
||||
* on a dedicated line so the user sees WHY (e.g. ``Configured sandbox
|
||||
* backend "nsjail" is unavailable``) instead of just the generic
|
||||
* "unavailable" wording. Ignored when ``hint === 'boxDisabled'``
|
||||
* because the disabled-by-config message already carries the reason. */
|
||||
reason?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function BoxUnavailableNotice({
|
||||
hint,
|
||||
context,
|
||||
reason,
|
||||
className,
|
||||
}: BoxUnavailableNoticeProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -30,13 +32,16 @@ export function BoxUnavailableNotice({
|
||||
|
||||
const variant = hint === 'boxDisabled' ? 'default' : 'destructive';
|
||||
const Icon = hint === 'boxDisabled' ? Info : ShieldAlert;
|
||||
const showReason = hint === 'boxUnavailable' && reason;
|
||||
|
||||
return (
|
||||
<Alert variant={variant} className={className}>
|
||||
<Icon className="h-4 w-4" />
|
||||
<AlertDescription className="space-y-1">
|
||||
<div>{t(`monitoring.${hint}`)}</div>
|
||||
{context && <div className="text-xs opacity-80">{context}</div>}
|
||||
{showReason && (
|
||||
<div className="text-xs font-mono opacity-80 break-all">{reason}</div>
|
||||
)}
|
||||
<div className="text-xs opacity-80">
|
||||
{t('monitoring.boxRequiredHint')}
|
||||
</div>
|
||||
|
||||
@@ -421,7 +421,11 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
);
|
||||
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const watchMode = form.watch('mode');
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const {
|
||||
available: boxAvailable,
|
||||
hint: boxHint,
|
||||
reason: boxReason,
|
||||
} = useBoxStatus();
|
||||
// stdio mode requires the Box sandbox at runtime. If the user picks
|
||||
// stdio while Box is disabled / unreachable, the server would refuse
|
||||
// to start anyway — block creation upfront so they aren't surprised
|
||||
@@ -868,7 +872,11 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{stdioBlockedByBox && (
|
||||
<BoxUnavailableNotice hint={boxHint} className="mt-2" />
|
||||
<BoxUnavailableNotice
|
||||
hint={boxHint}
|
||||
reason={boxReason}
|
||||
className="mt-2"
|
||||
/>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -28,7 +28,11 @@ export default function PipelineExtension({
|
||||
pipelineId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const {
|
||||
available: boxAvailable,
|
||||
hint: boxHint,
|
||||
reason: boxReason,
|
||||
} = useBoxStatus();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [enableAllPlugins, setEnableAllPlugins] = useState(true);
|
||||
const [enableAllMCPServers, setEnableAllMCPServers] = useState(true);
|
||||
@@ -526,7 +530,9 @@ export default function PipelineExtension({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!boxAvailable && <BoxUnavailableNotice hint={boxHint} />}
|
||||
{!boxAvailable && (
|
||||
<BoxUnavailableNotice hint={boxHint} reason={boxReason} />
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{enableAllSkills ? (
|
||||
<div className="flex h-32 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30">
|
||||
|
||||
@@ -239,7 +239,11 @@ export default function MCPFormDialog({
|
||||
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const watchMode = form.watch('mode');
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const {
|
||||
available: boxAvailable,
|
||||
hint: boxHint,
|
||||
reason: boxReason,
|
||||
} = useBoxStatus();
|
||||
// stdio mode requires the Box sandbox at runtime. Block creation here
|
||||
// so users aren't surprised by a connection failure on the detail page.
|
||||
const stdioBlockedByBox = watchMode === 'stdio' && !boxAvailable;
|
||||
@@ -676,7 +680,11 @@ export default function MCPFormDialog({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{stdioBlockedByBox && (
|
||||
<BoxUnavailableNotice hint={boxHint} className="mt-2" />
|
||||
<BoxUnavailableNotice
|
||||
hint={boxHint}
|
||||
reason={boxReason}
|
||||
className="mt-2"
|
||||
/>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -32,7 +32,11 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
const { refreshSkills, skills, setDetailEntityName } = useSidebarData();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const skill = skills.find((item) => item.id === id);
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const {
|
||||
available: boxAvailable,
|
||||
hint: boxHint,
|
||||
reason: boxReason,
|
||||
} = useBoxStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreateMode) {
|
||||
@@ -96,7 +100,7 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
<BoxUnavailableNotice hint={boxHint} reason={boxReason} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -176,7 +180,7 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
<BoxUnavailableNotice hint={boxHint} reason={boxReason} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ export default function SkillsPage() {
|
||||
const { refreshSkills } = useSidebarData();
|
||||
|
||||
const isCreateView = actionParam === 'create';
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const {
|
||||
available: boxAvailable,
|
||||
hint: boxHint,
|
||||
reason: boxReason,
|
||||
} = useBoxStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailId && !isCreateView) {
|
||||
@@ -64,7 +68,7 @@ export default function SkillsPage() {
|
||||
</div>
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
<BoxUnavailableNotice hint={boxHint} reason={boxReason} />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-h-0 flex-1">
|
||||
|
||||
@@ -51,6 +51,14 @@ export function useBoxStatus(refreshMs = 30_000) {
|
||||
: status
|
||||
? 'boxUnavailable'
|
||||
: null;
|
||||
// Specific reason from the backend (e.g.
|
||||
// ``Configured sandbox backend "nsjail" is unavailable`` or
|
||||
// ``docker daemon not running``). Surface this in the failed-state
|
||||
// banner so the user sees WHY instead of a generic "unavailable".
|
||||
// For the disabled-by-config case the boxDisabled i18n string already
|
||||
// carries the reason, so we suppress the duplicate.
|
||||
const reason =
|
||||
hint === 'boxUnavailable' ? status?.connector_error?.trim() || null : null;
|
||||
|
||||
return { status, loading, available, disabled, hint, refresh };
|
||||
return { status, loading, available, disabled, hint, reason, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user