mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
feat(web): surface Box disabled/unavailable state across consumers
When Box is disabled in config (``box.enabled = false``) or fails to connect, every dependent UI surface now degrades visibly: - ``useBoxStatus`` hook: shared, polled 30s, exposes ``available``, ``disabled`` (config-off) and a single ``hint`` key so callers don't have to re-derive the three states - ``BoxUnavailableNotice`` reusable Alert banner driven by that hint - Dashboard SystemStatusCards: three-state dot + label (connected / disabled-gray / disconnected-red); disabled state shows the ``boxDisabled`` hint, failed state continues to show the connector error. Plugin block kept untouched - Skills page (create view) and SkillDetailContent (edit view): Save button disabled and banner inserted above the form when Box is unavailable — matches the backend gate added in the previous commit - PipelineExtension skill section: ``enable_all_skills`` switch, Add Skill button and Remove buttons all gate on Box availability; banner inline under the section header - PipelineFormComponent: banner above the ``local-agent`` stage card when Box is unavailable, since that stage carries the sandbox-bound ``box-session-id-template`` field - Box status payload type (``ApiRespBoxStatus.enabled``) and 8 locale files updated with ``boxDisabled`` / ``boxUnavailable`` / ``boxRequiredHint`` strings Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
48
web/src/app/home/components/BoxUnavailableNotice.tsx
Normal file
48
web/src/app/home/components/BoxUnavailableNotice.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Info, ShieldAlert } from 'lucide-react';
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
/**
|
||||
* Banner shown when a feature depends on the Box sandbox runtime but it is
|
||||
* currently disabled in config or otherwise unavailable. Pass the ``hint``
|
||||
* key returned by ``useBoxStatus`` (``'boxDisabled' | 'boxUnavailable'``).
|
||||
*
|
||||
* Renders nothing when there is no hint — safe to drop at the top of any
|
||||
* page that may or may not need to surface the notice.
|
||||
*/
|
||||
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. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function BoxUnavailableNotice({
|
||||
hint,
|
||||
context,
|
||||
className,
|
||||
}: BoxUnavailableNoticeProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!hint) return null;
|
||||
|
||||
const variant = hint === 'boxDisabled' ? 'default' : 'destructive';
|
||||
const Icon = hint === 'boxDisabled' ? Info : ShieldAlert;
|
||||
|
||||
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>}
|
||||
<div className="text-xs opacity-80">
|
||||
{t('monitoring.boxRequiredHint')}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoxUnavailableNotice;
|
||||
@@ -36,14 +36,16 @@ import {
|
||||
} from '@/components/ui/tooltip';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
|
||||
function StatusDot({ ok }: { ok: boolean | null }) {
|
||||
if (ok === null)
|
||||
type StatusState = 'ok' | 'disabled' | 'failed' | null;
|
||||
|
||||
function StatusDot({ state }: { state: StatusState }) {
|
||||
if (state === null)
|
||||
return <span className="w-2 h-2 rounded-full bg-muted-foreground/40" />;
|
||||
return ok ? (
|
||||
<span className="w-2 h-2 rounded-full bg-green-500" />
|
||||
) : (
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
);
|
||||
if (state === 'ok')
|
||||
return <span className="w-2 h-2 rounded-full bg-green-500" />;
|
||||
if (state === 'disabled')
|
||||
return <span className="w-2 h-2 rounded-full bg-muted-foreground/60" />;
|
||||
return <span className="w-2 h-2 rounded-full bg-red-500" />;
|
||||
}
|
||||
|
||||
interface SystemStatusCardProps {
|
||||
@@ -86,7 +88,25 @@ export default function SystemStatusCard({
|
||||
const pluginOk = pluginStatus
|
||||
? pluginStatus.is_enable && pluginStatus.is_connected
|
||||
: null;
|
||||
const pluginState: StatusState = pluginStatus
|
||||
? pluginStatus.is_enable && pluginStatus.is_connected
|
||||
? 'ok'
|
||||
: !pluginStatus.is_enable
|
||||
? 'disabled'
|
||||
: 'failed'
|
||||
: null;
|
||||
const boxOk = boxStatus ? boxStatus.available : null;
|
||||
// Box has three observable states: connected (ok), disabled by config
|
||||
// (enabled = false → distinct gray dot + "disabled" hint), and configured
|
||||
// but failed (red dot + connector_error). The dashboard must distinguish
|
||||
// them so operators can tell intentional-off from misconfigured.
|
||||
const boxState: StatusState = boxStatus
|
||||
? boxStatus.available
|
||||
? 'ok'
|
||||
: boxStatus.enabled === false
|
||||
? 'disabled'
|
||||
: 'failed'
|
||||
: null;
|
||||
|
||||
const handleOpenDialog = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -129,12 +149,12 @@ export default function SystemStatusCard({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot ok={pluginOk} />
|
||||
<StatusDot state={pluginState} />
|
||||
<Plug className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-sm">{t('monitoring.pluginRuntime')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot ok={boxOk} />
|
||||
<StatusDot state={boxState} />
|
||||
<Box className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-sm">{t('monitoring.boxRuntime')}</span>
|
||||
</div>
|
||||
@@ -207,24 +227,39 @@ export default function SystemStatusCard({
|
||||
</div>
|
||||
<div className="ml-6 text-sm space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{boxOk ? (
|
||||
{boxState === 'ok' ? (
|
||||
<CircleCheck className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<CircleX className="w-4 h-4 text-red-500" />
|
||||
<CircleX
|
||||
className={
|
||||
boxState === 'disabled'
|
||||
? 'w-4 h-4 text-muted-foreground'
|
||||
: 'w-4 h-4 text-red-500'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
boxOk
|
||||
boxState === 'ok'
|
||||
? 'text-green-600 font-medium'
|
||||
: 'text-red-500 font-medium'
|
||||
: boxState === 'disabled'
|
||||
? 'text-muted-foreground font-medium'
|
||||
: 'text-red-500 font-medium'
|
||||
}
|
||||
>
|
||||
{boxOk
|
||||
{boxState === 'ok'
|
||||
? t('monitoring.connected')
|
||||
: t('monitoring.disconnected')}
|
||||
: boxState === 'disabled'
|
||||
? t('monitoring.disabled')
|
||||
: t('monitoring.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
{boxStatus && !boxOk && boxStatus.connector_error && (
|
||||
{boxState === 'disabled' && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('monitoring.boxDisabled')}
|
||||
</p>
|
||||
)}
|
||||
{boxState === 'failed' && boxStatus?.connector_error && (
|
||||
<p className="text-red-400 text-xs break-all">
|
||||
{boxStatus.connector_error}
|
||||
</p>
|
||||
|
||||
@@ -19,6 +19,8 @@ import { Label } from '@/components/ui/label';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import { MCPServer, Skill } from '@/app/infra/entities/api';
|
||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
|
||||
export default function PipelineExtension({
|
||||
pipelineId,
|
||||
@@ -26,6 +28,7 @@ export default function PipelineExtension({
|
||||
pipelineId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [enableAllPlugins, setEnableAllPlugins] = useState(true);
|
||||
const [enableAllMCPServers, setEnableAllMCPServers] = useState(true);
|
||||
@@ -519,9 +522,11 @@ export default function PipelineExtension({
|
||||
id="enable-all-skills"
|
||||
checked={enableAllSkills}
|
||||
onCheckedChange={handleToggleEnableAllSkills}
|
||||
disabled={!boxAvailable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!boxAvailable && <BoxUnavailableNotice hint={boxHint} />}
|
||||
<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">
|
||||
@@ -559,6 +564,7 @@ export default function PipelineExtension({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveSkill(skill.name)}
|
||||
disabled={!boxAvailable}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -572,7 +578,7 @@ export default function PipelineExtension({
|
||||
onClick={handleOpenSkillDialog}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={enableAllSkills}
|
||||
disabled={enableAllSkills || !boxAvailable}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('pipelines.extensions.addSkill')}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
|
||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -75,6 +77,7 @@ export default function PipelineFormComponent({
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showCopyConfirm, setShowCopyConfirm] = useState(false);
|
||||
const [isDefaultPipeline, setIsDefaultPipeline] = useState<boolean>(false);
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
|
||||
const formSchema = isEditMode
|
||||
? z.object({
|
||||
@@ -413,29 +416,40 @@ export default function PipelineFormComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// The local-agent stage carries sandbox-bound fields (e.g.
|
||||
// ``box-session-id-template``). When Box is disabled / unavailable, show
|
||||
// a banner so operators know those fields will have no effect. We render
|
||||
// the banner above the card rather than per-field because the field set
|
||||
// is driven by yaml metadata and a single notice keeps the UI calm.
|
||||
const showBoxNoticeForStage = stage.name === 'local-agent' && !boxAvailable;
|
||||
|
||||
return (
|
||||
<Card key={stage.name}>
|
||||
<CardHeader>
|
||||
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
|
||||
{stage.description && (
|
||||
<CardDescription>
|
||||
{extractI18nObject(stage.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div key={stage.name} className="space-y-3">
|
||||
{showBoxNoticeForStage && <BoxUnavailableNotice hint={boxHint} />}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
|
||||
{stage.description && (
|
||||
<CardDescription>
|
||||
{extractI18nObject(stage.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import SkillForm from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
import { Sparkles, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function SkillDetailContent({ id }: { id: string }) {
|
||||
@@ -30,6 +32,7 @@ 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();
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreateMode) {
|
||||
@@ -81,11 +84,22 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" form="skill-form" className="shrink-0">
|
||||
<Button
|
||||
type="submit"
|
||||
form="skill-form"
|
||||
className="shrink-0"
|
||||
disabled={!boxAvailable}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<SkillForm
|
||||
key="new-skill"
|
||||
@@ -150,11 +164,22 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" form="skill-form" className="shrink-0">
|
||||
<Button
|
||||
type="submit"
|
||||
form="skill-form"
|
||||
className="shrink-0"
|
||||
disabled={!boxAvailable}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<SkillForm
|
||||
key={id}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Button } from '@/components/ui/button';
|
||||
import SkillDetailContent from '@/app/home/skills/SkillDetailContent';
|
||||
import SkillForm from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
|
||||
export default function SkillsPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -15,6 +17,7 @@ export default function SkillsPage() {
|
||||
const { refreshSkills } = useSidebarData();
|
||||
|
||||
const isCreateView = actionParam === 'create';
|
||||
const { available: boxAvailable, hint: boxHint } = useBoxStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailId && !isCreateView) {
|
||||
@@ -54,11 +57,16 @@ export default function SkillsPage() {
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" form="skill-form">
|
||||
<Button type="submit" form="skill-form" disabled={!boxAvailable}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!boxAvailable && (
|
||||
<div className="pb-4 shrink-0">
|
||||
<BoxUnavailableNotice hint={boxHint} />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-h-0 flex-1">
|
||||
<SkillForm
|
||||
key="new-skill"
|
||||
|
||||
@@ -362,6 +362,9 @@ export interface ApiRespPluginSystemStatus {
|
||||
|
||||
export interface ApiRespBoxStatus {
|
||||
available: boolean;
|
||||
/** Whether ``box.enabled`` is true in config. When false, the sandbox
|
||||
* is deliberately disabled — distinct from "configured but failed". */
|
||||
enabled?: boolean;
|
||||
profile: string;
|
||||
recent_error_count: number;
|
||||
connector_error?: string;
|
||||
|
||||
56
web/src/app/infra/hooks/useBoxStatus.ts
Normal file
56
web/src/app/infra/hooks/useBoxStatus.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { ApiRespBoxStatus } from '@/app/infra/entities/api';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
|
||||
/**
|
||||
* Shared hook for Box runtime status — used by every UI surface that needs
|
||||
* to gate behaviour on whether the sandbox is available. Returns:
|
||||
*
|
||||
* - status: full payload (or null while loading / on error)
|
||||
* - available: convenience flag (status?.available === true)
|
||||
* - disabled: true iff Box is explicitly disabled by config
|
||||
* (status.enabled === false), distinguishing it from
|
||||
* "configured but currently failed"
|
||||
* - hint: a single i18n-key choice for the banner message —
|
||||
* 'boxDisabled' / 'boxUnavailable' / null
|
||||
* - refresh: imperative re-fetch
|
||||
*
|
||||
* Polls every ``refreshMs`` (default 30s) so a flapping runtime is picked
|
||||
* up without a page reload.
|
||||
*/
|
||||
export function useBoxStatus(refreshMs = 30_000) {
|
||||
const [status, setStatus] = useState<ApiRespBoxStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const data = await httpClient.getBoxStatus();
|
||||
setStatus(data);
|
||||
} catch {
|
||||
// Keep last-known status; the dashboard polls separately so a
|
||||
// transient failure here should not blank the UI for sandbox
|
||||
// consumers.
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
const id = setInterval(() => void refresh(), refreshMs);
|
||||
return () => clearInterval(id);
|
||||
}, [refresh, refreshMs]);
|
||||
|
||||
const available = status?.available === true;
|
||||
const disabled = status?.available === false && status?.enabled === false;
|
||||
const hint: 'boxDisabled' | 'boxUnavailable' | null = available
|
||||
? null
|
||||
: disabled
|
||||
? 'boxDisabled'
|
||||
: status
|
||||
? 'boxUnavailable'
|
||||
: null;
|
||||
|
||||
return { status, loading, available, disabled, hint, refresh };
|
||||
}
|
||||
@@ -1303,6 +1303,12 @@ const enUS = {
|
||||
disabled: 'Disabled',
|
||||
statusDetail: 'Status',
|
||||
pluginDisabled: 'Plugin system is disabled',
|
||||
boxDisabled:
|
||||
'Box sandbox is disabled in config — sandbox tools, skill add/edit, and stdio MCP are unavailable',
|
||||
boxUnavailable:
|
||||
'Box sandbox is unavailable — sandbox tools, skill add/edit, and stdio MCP are unavailable',
|
||||
boxRequiredHint:
|
||||
'This feature requires the Box runtime. Enable it in config (box.enabled = true) and ensure the runtime is healthy.',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Profile',
|
||||
boxSandboxes: 'Sandboxes',
|
||||
|
||||
@@ -1336,6 +1336,12 @@ const esES = {
|
||||
disabled: 'Desactivado',
|
||||
statusDetail: 'Estado',
|
||||
pluginDisabled: 'El sistema de plugins está desactivado',
|
||||
boxDisabled:
|
||||
'El sandbox de Box está desactivado en la configuración — herramientas de sandbox, alta/edición de skills y MCP stdio no están disponibles',
|
||||
boxUnavailable:
|
||||
'El sandbox de Box no está disponible — herramientas de sandbox, alta/edición de skills y MCP stdio no están disponibles',
|
||||
boxRequiredHint:
|
||||
'Esta función requiere el runtime de Box. Actívelo en la configuración (box.enabled = true) y asegúrese de que el runtime está conectado.',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Perfil',
|
||||
boxSandboxes: 'Sandboxes',
|
||||
|
||||
@@ -1308,6 +1308,12 @@ const jaJP = {
|
||||
disabled: '無効',
|
||||
statusDetail: 'ステータス',
|
||||
pluginDisabled: 'プラグインシステムが無効です',
|
||||
boxDisabled:
|
||||
'Box サンドボックスは設定で無効化されています — サンドボックスツール / スキルの追加・編集 / stdio MCP は利用できません',
|
||||
boxUnavailable:
|
||||
'Box サンドボックスは利用できません — サンドボックスツール / スキルの追加・編集 / stdio MCP は利用できません',
|
||||
boxRequiredHint:
|
||||
'この機能には Box ランタイムが必要です。設定で有効化(box.enabled = true)し、ランタイムが正常に接続できることを確認してください。',
|
||||
boxBackend: 'バックエンド',
|
||||
boxProfile: 'プロファイル',
|
||||
boxSandboxes: 'サンドボックス',
|
||||
|
||||
@@ -1311,6 +1311,12 @@ const ruRU = {
|
||||
disabled: 'Отключено',
|
||||
statusDetail: 'Статус',
|
||||
pluginDisabled: 'Система плагинов отключена',
|
||||
boxDisabled:
|
||||
'Песочница Box отключена в конфигурации — инструменты песочницы, добавление/редактирование навыков и stdio MCP недоступны',
|
||||
boxUnavailable:
|
||||
'Песочница Box недоступна — инструменты песочницы, добавление/редактирование навыков и stdio MCP недоступны',
|
||||
boxRequiredHint:
|
||||
'Для этой функции требуется среда Box. Включите её в конфигурации (box.enabled = true) и убедитесь, что соединение работает.',
|
||||
boxBackend: 'Бэкенд',
|
||||
boxProfile: 'Профиль',
|
||||
boxSandboxes: 'Песочницы',
|
||||
|
||||
@@ -1282,6 +1282,12 @@ const thTH = {
|
||||
disabled: 'ปิดใช้งาน',
|
||||
statusDetail: 'สถานะ',
|
||||
pluginDisabled: 'ระบบปลั๊กอินถูกปิดใช้งาน',
|
||||
boxDisabled:
|
||||
'Sandbox Box ถูกปิดใช้งานในการตั้งค่า — เครื่องมือ sandbox, การเพิ่ม/แก้ไข skill และ stdio MCP ใช้งานไม่ได้',
|
||||
boxUnavailable:
|
||||
'Sandbox Box ไม่พร้อมใช้งาน — เครื่องมือ sandbox, การเพิ่ม/แก้ไข skill และ stdio MCP ใช้งานไม่ได้',
|
||||
boxRequiredHint:
|
||||
'ฟีเจอร์นี้ต้องใช้ Box runtime กรุณาเปิดใช้งานในการตั้งค่า (box.enabled = true) และตรวจสอบว่าการเชื่อมต่อปกติ',
|
||||
boxBackend: 'แบ็กเอนด์',
|
||||
boxProfile: 'โปรไฟล์',
|
||||
boxSandboxes: 'แซนด์บ็อกซ์',
|
||||
|
||||
@@ -1305,6 +1305,12 @@ const viVN = {
|
||||
disabled: 'Đã tắt',
|
||||
statusDetail: 'Trạng thái',
|
||||
pluginDisabled: 'Hệ thống plugin đã tắt',
|
||||
boxDisabled:
|
||||
'Sandbox Box đã tắt trong cấu hình — công cụ sandbox, thêm/chỉnh sửa skill và stdio MCP đều không khả dụng',
|
||||
boxUnavailable:
|
||||
'Sandbox Box không khả dụng — công cụ sandbox, thêm/chỉnh sửa skill và stdio MCP đều không khả dụng',
|
||||
boxRequiredHint:
|
||||
'Tính năng này cần Box runtime. Hãy bật trong cấu hình (box.enabled = true) và đảm bảo runtime đang hoạt động.',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Hồ sơ',
|
||||
boxSandboxes: 'Sandbox',
|
||||
|
||||
@@ -1249,6 +1249,12 @@ const zhHans = {
|
||||
disabled: '已禁用',
|
||||
statusDetail: '状态',
|
||||
pluginDisabled: '插件系统已禁用',
|
||||
boxDisabled:
|
||||
'Box 沙箱已在配置中禁用——沙箱工具、技能添加/编辑与 stdio MCP 均不可用',
|
||||
boxUnavailable:
|
||||
'Box 沙箱不可用——沙箱工具、技能添加/编辑与 stdio MCP 均不可用',
|
||||
boxRequiredHint:
|
||||
'此功能依赖 Box 运行时。请在配置中启用(box.enabled = true)并确认运行时连接正常。',
|
||||
boxBackend: '后端',
|
||||
boxProfile: '配置',
|
||||
boxSandboxes: '沙箱数',
|
||||
|
||||
@@ -1248,6 +1248,12 @@ const zhHant = {
|
||||
disabled: '已停用',
|
||||
statusDetail: '狀態',
|
||||
pluginDisabled: '外掛系統已停用',
|
||||
boxDisabled:
|
||||
'Box 沙箱已在設定中停用——沙箱工具、技能新增/編輯與 stdio MCP 均無法使用',
|
||||
boxUnavailable:
|
||||
'Box 沙箱無法使用——沙箱工具、技能新增/編輯與 stdio MCP 均無法使用',
|
||||
boxRequiredHint:
|
||||
'此功能需要 Box 執行時。請在設定中啟用(box.enabled = true)並確認執行時連線正常。',
|
||||
boxBackend: '後端',
|
||||
boxProfile: '設定檔',
|
||||
boxSandboxes: '沙箱數',
|
||||
|
||||
Reference in New Issue
Block a user