+ {collapsed ? "▸" : "▾"}
{stale && ⚠}
{label}
- {data.sublabel === "offline" && (
+ {stale &&
{t("sourceStale", { since })}
}
+ {data.sourceIssue === "offline" && (
{t("sourceOffline")}
)}
@@ -31,7 +47,7 @@ function SourceNodeImpl({ data }: { data: OrchNode }) {
{data.counts?.[s]}
@@ -44,3 +60,4 @@ function SourceNodeImpl({ data }: { data: OrchNode }) {
}
export const SourceNode = memo(SourceNodeImpl);
+SourceNode.displayName = "SourceNode";
diff --git a/src/app/(dashboard)/dashboard/orchestration/nodes/WorkNode.tsx b/src/app/(dashboard)/dashboard/orchestration/nodes/WorkNode.tsx
index ed7d28cdfd4..4f678f7e508 100644
--- a/src/app/(dashboard)/dashboard/orchestration/nodes/WorkNode.tsx
+++ b/src/app/(dashboard)/dashboard/orchestration/nodes/WorkNode.tsx
@@ -43,3 +43,4 @@ function WorkNodeImpl({ data }: { data: OrchNode }) {
}
export const WorkNode = memo(WorkNodeImpl);
+WorkNode.displayName = "WorkNode";
diff --git a/src/app/(dashboard)/dashboard/orchestration/page.tsx b/src/app/(dashboard)/dashboard/orchestration/page.tsx
index b86f67ede08..bc674c12755 100644
--- a/src/app/(dashboard)/dashboard/orchestration/page.tsx
+++ b/src/app/(dashboard)/dashboard/orchestration/page.tsx
@@ -1,7 +1,16 @@
-import type { Metadata } from "next";
+import { Suspense } from "react";
+import { getTranslations } from "next-intl/server";
import OrchestrationPageClient from "./OrchestrationPageClient";
-export const metadata: Metadata = { title: "Orchestration — OmniRoute" };
+export async function generateMetadata() {
+ const t = await getTranslations("orchestration");
+ return { title: t("title"), description: t("description") };
+}
+
export default function OrchestrationPage() {
- return
;
+ return (
+
+
+
+ );
}
diff --git a/src/app/(dashboard)/dashboard/orchestration/tabs/AgentsTab.tsx b/src/app/(dashboard)/dashboard/orchestration/tabs/AgentsTab.tsx
index 189f258d76e..58a9cd19bdc 100644
--- a/src/app/(dashboard)/dashboard/orchestration/tabs/AgentsTab.tsx
+++ b/src/app/(dashboard)/dashboard/orchestration/tabs/AgentsTab.tsx
@@ -1,16 +1,17 @@
"use client";
import { useMemo } from "react";
-import type { NodeTypes, NodeMouseHandler } from "@xyflow/react";
+import type { NodeTypes, EdgeTypes, NodeMouseHandler } from "@xyflow/react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { FlowCanvas } from "@/shared/components/flow/FlowCanvas";
import { orchestrationToFlow } from "../model/orchestrationToFlow";
-import type { OrchSnapshot } from "../model/orchestrationTypes";
+import type { OrchNode, OrchSnapshot, OrchSource } from "../model/orchestrationTypes";
import { OrchestratorNode } from "../nodes/OrchestratorNode";
import { SourceNode } from "../nodes/SourceNode";
import { WorkNode } from "../nodes/WorkNode";
import { ActivityNode } from "../nodes/ActivityNode";
import { OverflowNode } from "../nodes/OverflowNode";
+import { StatusEdge } from "../edges/StatusEdge";
const NODE_TYPES: NodeTypes = {
orchestrator: OrchestratorNode as never,
@@ -19,22 +20,40 @@ const NODE_TYPES: NodeTypes = {
activity: ActivityNode as never,
overflow: OverflowNode as never,
};
+const EDGE_TYPES: EdgeTypes = { status: StatusEdge as never };
+
+// Stable empty-set reference — avoids re-minting a Set every render when the caller
+// doesn't pass `collapsed` (e.g. pre-A6 callers/tests), so orchestrationToFlow's memo
+// doesn't invalidate on every render.
+const EMPTY_COLLAPSED: ReadonlySet
= new Set();
export function AgentsTab({
snapshot,
onNodeClick,
showCompleted,
onToggleCompleted,
+ collapsed = EMPTY_COLLAPSED,
+ onToggleCollapse,
}: {
snapshot: OrchSnapshot;
onNodeClick: (orchNodeId: string) => void;
showCompleted: boolean;
onToggleCompleted: (v: boolean) => void;
+ collapsed?: ReadonlySet;
+ onToggleCollapse?: (s: OrchSource) => void;
}) {
const t = useTranslations("orchestration");
- const { nodes, edges, fitKey } = useMemo(() => orchestrationToFlow(snapshot), [snapshot]);
+ const { nodes, edges, fitKey } = useMemo(
+ () => orchestrationToFlow(snapshot, { collapsed }),
+ [snapshot, collapsed]
+ );
const hasWork = snapshot.nodes.some((n) => n.kind === "work");
const handleClick: NodeMouseHandler = (_e, node) => {
+ if (node.type === "source") {
+ const source = (node.data as unknown as OrchNode).source;
+ if (source) onToggleCollapse?.(source);
+ return;
+ }
if (node.type === "work" || node.type === "activity" || node.type === "overflow")
onNodeClick(node.id);
};
@@ -71,6 +90,7 @@ export function AgentsTab({
nodes={nodes}
edges={edges}
nodeTypes={NODE_TYPES}
+ edgeTypes={EDGE_TYPES}
fitKey={fitKey}
onNodeClick={handleClick}
className="h-full"
diff --git a/src/app/(dashboard)/dashboard/orchestration/tabs/OverviewTab.tsx b/src/app/(dashboard)/dashboard/orchestration/tabs/OverviewTab.tsx
index 912882b0a45..bceca883a08 100644
--- a/src/app/(dashboard)/dashboard/orchestration/tabs/OverviewTab.tsx
+++ b/src/app/(dashboard)/dashboard/orchestration/tabs/OverviewTab.tsx
@@ -22,6 +22,7 @@ const STATE_KEY: Record = {
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function formatElapsed(ms: number): string {
+ if (!Number.isFinite(ms)) return "—";
const s = Math.max(0, Math.floor(ms / 1000));
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
}
diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx
index 8290c229edd..a65478dd2a1 100644
--- a/src/app/(dashboard)/dashboard/providers/page.tsx
+++ b/src/app/(dashboard)/dashboard/providers/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback, useMemo } from "react";
+import { useState, useEffect, useCallback, useMemo, Suspense } from "react";
import { Card, CardSkeleton, Badge, Button, CollapsibleSection } from "@/shared/components";
import {
AGGREGATOR_PROVIDER_IDS,
@@ -211,7 +211,7 @@ async function loadOauthEnvRepairStatus(): Promise<{
}
}
-export default function ProvidersPage() {
+function ProvidersPageContent() {
const router = useRouter();
const [connections, setConnections] = useState([]);
const [providerNodes, setProviderNodes] = useState([]);
@@ -1895,6 +1895,14 @@ export default function ProvidersPage() {
);
}
+export default function ProvidersPage() {
+ return (
+
+
+
+ );
+}
+
// ─── Provider Test Results View (mirrors combo TestResultsView) ──────────────
function ProviderTestResultsView({ results }: { results: ProviderBatchTestResults }) {
diff --git a/src/app/(dashboard)/dashboard/radar/setup/page.tsx b/src/app/(dashboard)/dashboard/radar/setup/page.tsx
index fde7d36d4ef..30841612386 100644
--- a/src/app/(dashboard)/dashboard/radar/setup/page.tsx
+++ b/src/app/(dashboard)/dashboard/radar/setup/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback, Suspense } from "react";
import { useLocale, useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
@@ -48,7 +48,7 @@ function resolveText(text: RadarLocalizedText, locale: string): string {
// Component
// ---------------------------------------------------------------------------
-export default function RadarSetupPage() {
+function RadarSetupPageContent() {
const t = useTranslations("radarSetupPage");
const locale = useLocale();
const searchParams = useSearchParams();
@@ -290,3 +290,11 @@ export default function RadarSetupPage() {
);
}
+
+export default function RadarSetupPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 1e2d79deef2..1d2229e3552 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,6 +48,13 @@
--color-success: #22c55e;
--color-warning: #f59e0b;
+ /* Orchestration status tokens — light values (AA sobre fundo claro) */
+ --orch-status-success: #15803d;
+ --orch-status-warning: #b45309;
+ --orch-status-error: #b91c1c;
+ --orch-status-muted: #6b7280;
+ --orch-status-approval: #6d28d9;
+
/* Traffic lights */
--color-traffic-red: #ff5f56;
--color-traffic-yellow: #ffbd2e;
@@ -130,6 +137,13 @@
--color-text-primary: #e6e6ef;
--color-text-muted: #a1a1aa;
+ /* Orchestration status tokens — dark values (previous STATUS_HEX constants) */
+ --orch-status-success: #22c55e;
+ --orch-status-warning: #f59e0b;
+ --orch-status-error: #ef4444;
+ --orch-status-muted: #6b7280;
+ --orch-status-approval: #8b5cf6;
+
/* Fumadocs theme mapping */
--color-fd-background: #0b0e14;
--color-fd-foreground: #e6e6ef;
@@ -826,4 +840,7 @@ html[dir="rtl"] :where(.material-symbols-outlined, .traffic-lights, .react-flow)
animation: none;
stroke-dasharray: none;
}
+ .orchestration-canvas .orch-edge-particle {
+ display: none;
+ }
}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index a7eb476cefe..2bc8cf6797b 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -13935,11 +13935,18 @@
"tabRouting": "التوجيه",
"tabOverview": "نظرة عامة",
"showCompleted": "إظهار المكتمل",
+ "searchPlaceholder": "البحث في المهام…",
+ "filterStates": "الحالات",
+ "filterSources": "المصادر",
+ "filterProviders": "المزوّدون",
+ "clearFilters": "مسح عوامل التصفية",
"sourceCloudAgent": "وكيل سحابي",
"sourceA2A": "A2A",
"sourceConductor": "موصل",
"sourceStale": "قديم منذ {since}",
"sourceOffline": "غير متصل",
+ "sourceCollapse": "طيّ",
+ "sourceExpand": "توسيع",
"stateQueued": "في الانتظار",
"stateRunning": "قيد التشغيل",
"stateWaitingApproval": "بانتظار الموافقة",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "المقاييس",
"drawerResult": "النتيجة",
"drawerActions": "الإجراءات",
+ "drawerClose": "إغلاق",
+ "copyTrace": "نسخ أثر التتبع (JSON)",
"actionApprove": "الموافقة على الخطة",
"actionCancel": "إلغاء",
"actionSeeInGraph": "عرض في الرسم البياني",
"actionDone": "تم تطبيق الإجراء",
"actionFailed": "فشل الإجراء: {error}",
+ "detailFailed": "فشل تحميل التفاصيل: {error}",
"mirroredInA2A": "معكوس في A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json
index 5f96480171d..8ccd7f43d51 100644
--- a/src/i18n/messages/az.json
+++ b/src/i18n/messages/az.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Marşrutlaşdırma",
"tabOverview": "İcmal",
"showCompleted": "Tamamlananları göstər",
+ "searchPlaceholder": "Tapşırıqları axtar…",
+ "filterStates": "Statuslar",
+ "filterSources": "Mənbələr",
+ "filterProviders": "Provayderlər",
+ "clearFilters": "Filtrləri təmizlə",
"sourceCloudAgent": "Bulud agenti",
"sourceA2A": "A2A",
"sourceConductor": "İdarəçi",
"sourceStale": "{since} tarixindən köhnəlib",
"sourceOffline": "Oflayn",
+ "sourceCollapse": "Yığ",
+ "sourceExpand": "Aç",
"stateQueued": "Növbədə",
"stateRunning": "İşləyir",
"stateWaitingApproval": "Təsdiq gözləyir",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metriklər",
"drawerResult": "Nəticə",
"drawerActions": "Əməliyyatlar",
+ "drawerClose": "Bağla",
+ "copyTrace": "İzləmə JSON-unu kopyala",
"actionApprove": "Planı təsdiqlə",
"actionCancel": "Ləğv et",
"actionSeeInGraph": "Qrafikdə bax",
"actionDone": "Əməliyyat tətbiq edildi",
"actionFailed": "Əməliyyat uğursuz oldu: {error}",
+ "detailFailed": "Detallar yüklənmədi: {error}",
"mirroredInA2A": "A2A-da əks olunub"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json
index 32929ff42fd..c2f66e715db 100644
--- a/src/i18n/messages/bg.json
+++ b/src/i18n/messages/bg.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Маршрутизиране",
"tabOverview": "Общ преглед",
"showCompleted": "Показване на завършените",
+ "searchPlaceholder": "Търсене на задачи…",
+ "filterStates": "Състояния",
+ "filterSources": "Източници",
+ "filterProviders": "Доставчици",
+ "clearFilters": "Изчистване на филтрите",
"sourceCloudAgent": "Облачен агент",
"sourceA2A": "A2A",
"sourceConductor": "Кондуктор",
"sourceStale": "Остаряло от {since}",
"sourceOffline": "Офлайн",
+ "sourceCollapse": "Свиване",
+ "sourceExpand": "Разгъване",
"stateQueued": "На опашка",
"stateRunning": "Изпълнява се",
"stateWaitingApproval": "Изчаква одобрение",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Метрики",
"drawerResult": "Резултат",
"drawerActions": "Действия",
+ "drawerClose": "Затваряне",
+ "copyTrace": "Копиране на трасето (JSON)",
"actionApprove": "Одобри плана",
"actionCancel": "Отказ",
"actionSeeInGraph": "Виж в графа",
"actionDone": "Действието е приложено",
"actionFailed": "Действието се провали: {error}",
+ "detailFailed": "Неуспешно зареждане на детайлите: {error}",
"mirroredInA2A": "Отразено в A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json
index 331c2236851..87644a1d2d3 100644
--- a/src/i18n/messages/bn.json
+++ b/src/i18n/messages/bn.json
@@ -13935,11 +13935,18 @@
"tabRouting": "রাউটিং",
"tabOverview": "সংক্ষিপ্ত বিবরণ",
"showCompleted": "সম্পন্ন দেখান",
+ "searchPlaceholder": "কাজ অনুসন্ধান করুন…",
+ "filterStates": "অবস্থা",
+ "filterSources": "উৎস",
+ "filterProviders": "প্রদানকারী",
+ "clearFilters": "ফিল্টার সাফ করুন",
"sourceCloudAgent": "ক্লাউড এজেন্ট",
"sourceA2A": "A2A",
"sourceConductor": "কন্ডাক্টর",
"sourceStale": "{since} থেকে পুরনো",
"sourceOffline": "অফলাইন",
+ "sourceCollapse": "সংকুচিত করুন",
+ "sourceExpand": "প্রসারিত করুন",
"stateQueued": "সারিতে",
"stateRunning": "চলছে",
"stateWaitingApproval": "অনুমোদনের অপেক্ষায়",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "মেট্রিক্স",
"drawerResult": "ফলাফল",
"drawerActions": "কার্যক্রম",
+ "drawerClose": "বন্ধ করুন",
+ "copyTrace": "ট্রেস JSON কপি করুন",
"actionApprove": "পরিকল্পনা অনুমোদন করুন",
"actionCancel": "বাতিল করুন",
"actionSeeInGraph": "গ্রাফে দেখুন",
"actionDone": "কার্যক্রম প্রয়োগ করা হয়েছে",
"actionFailed": "কার্যক্রম ব্যর্থ হয়েছে: {error}",
+ "detailFailed": "বিবরণ লোড করতে ব্যর্থ: {error}",
"mirroredInA2A": "A2A-তে প্রতিফলিত"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json
index 006c276ee31..e94d2035837 100644
--- a/src/i18n/messages/cs.json
+++ b/src/i18n/messages/cs.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Směrování",
"tabOverview": "Přehled",
"showCompleted": "Zobrazit dokončené",
+ "searchPlaceholder": "Hledat úlohy…",
+ "filterStates": "Stavy",
+ "filterSources": "Zdroje",
+ "filterProviders": "Poskytovatelé",
+ "clearFilters": "Vymazat filtry",
"sourceCloudAgent": "Cloudový agent",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Neaktuální od {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Sbalit",
+ "sourceExpand": "Rozbalit",
"stateQueued": "Ve frontě",
"stateRunning": "Běží",
"stateWaitingApproval": "Čeká na schválení",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metriky",
"drawerResult": "Výsledek",
"drawerActions": "Akce",
+ "drawerClose": "Zavřít",
+ "copyTrace": "Kopírovat trasování (JSON)",
"actionApprove": "Schválit plán",
"actionCancel": "Zrušit",
"actionSeeInGraph": "Zobrazit v grafu",
"actionDone": "Akce provedena",
"actionFailed": "Akce selhala: {error}",
+ "detailFailed": "Nepodařilo se načíst podrobnosti: {error}",
"mirroredInA2A": "Zrcadleno v A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json
index 904a3a3147b..e7cd53d6347 100644
--- a/src/i18n/messages/da.json
+++ b/src/i18n/messages/da.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routing",
"tabOverview": "Oversigt",
"showCompleted": "Vis fuldførte",
+ "searchPlaceholder": "Søg i opgaver…",
+ "filterStates": "Tilstande",
+ "filterSources": "Kilder",
+ "filterProviders": "Udbydere",
+ "clearFilters": "Ryd filtre",
"sourceCloudAgent": "Cloud-agent",
"sourceA2A": "A2A",
"sourceConductor": "Konduktør",
"sourceStale": "Forældet siden {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Fold sammen",
+ "sourceExpand": "Fold ud",
"stateQueued": "I kø",
"stateRunning": "Kører",
"stateWaitingApproval": "Afventer godkendelse",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Målinger",
"drawerResult": "Resultat",
"drawerActions": "Handlinger",
+ "drawerClose": "Luk",
+ "copyTrace": "Kopiér sporing (JSON)",
"actionApprove": "Godkend plan",
"actionCancel": "Annuller",
"actionSeeInGraph": "Se i graf",
"actionDone": "Handling udført",
"actionFailed": "Handling mislykkedes: {error}",
+ "detailFailed": "Kunne ikke indlæse detaljer: {error}",
"mirroredInA2A": "Spejlet i A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 090162fbda9..72f7d99b782 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -13942,11 +13942,18 @@
"tabRouting": "Routing",
"tabOverview": "Übersicht",
"showCompleted": "Abgeschlossene anzeigen",
+ "searchPlaceholder": "Aufgaben durchsuchen…",
+ "filterStates": "Status",
+ "filterSources": "Quellen",
+ "filterProviders": "Anbieter",
+ "clearFilters": "Filter zurücksetzen",
"sourceCloudAgent": "Cloud-Agent",
"sourceA2A": "A2A",
"sourceConductor": "Leiter",
"sourceStale": "Veraltet seit {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Einklappen",
+ "sourceExpand": "Ausklappen",
"stateQueued": "In Warteschlange",
"stateRunning": "Läuft",
"stateWaitingApproval": "Wartet auf Genehmigung",
@@ -13963,11 +13970,14 @@
"drawerMetrics": "Metriken",
"drawerResult": "Ergebnis",
"drawerActions": "Aktionen",
+ "drawerClose": "Schließen",
+ "copyTrace": "Trace-JSON kopieren",
"actionApprove": "Plan genehmigen",
"actionCancel": "Abbrechen",
"actionSeeInGraph": "Im Graph anzeigen",
"actionDone": "Aktion angewendet",
"actionFailed": "Aktion fehlgeschlagen: {error}",
+ "detailFailed": "Details konnten nicht geladen werden: {error}",
"mirroredInA2A": "In A2A gespiegelt"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index aa7f379693f..6ab642e492a 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -13939,11 +13939,18 @@
"tabRouting": "Routing",
"tabOverview": "Overview",
"showCompleted": "Show completed",
+ "searchPlaceholder": "Search tasks…",
+ "filterStates": "States",
+ "filterSources": "Sources",
+ "filterProviders": "Providers",
+ "clearFilters": "Clear filters",
"sourceCloudAgent": "Cloud Agent",
"sourceA2A": "A2A",
"sourceConductor": "Conductor",
"sourceStale": "Stale since {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Collapse",
+ "sourceExpand": "Expand",
"stateQueued": "Queued",
"stateRunning": "Running",
"stateWaitingApproval": "Waiting approval",
@@ -13960,11 +13967,14 @@
"drawerMetrics": "Metrics",
"drawerResult": "Result",
"drawerActions": "Actions",
+ "drawerClose": "Close",
+ "copyTrace": "Copy trace JSON",
"actionApprove": "Approve plan",
"actionCancel": "Cancel",
"actionSeeInGraph": "See in graph",
"actionDone": "Action applied",
"actionFailed": "Action failed: {error}",
+ "detailFailed": "Failed to load details: {error}",
"mirroredInA2A": "Mirrored in A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index 4953d0469f5..9549997df26 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Enrutamiento",
"tabOverview": "Resumen",
"showCompleted": "Mostrar completados",
+ "searchPlaceholder": "Buscar tareas…",
+ "filterStates": "Estados",
+ "filterSources": "Fuentes",
+ "filterProviders": "Proveedores",
+ "clearFilters": "Borrar filtros",
"sourceCloudAgent": "Agente en la nube",
"sourceA2A": "A2A",
"sourceConductor": "Conductor",
"sourceStale": "Obsoleto desde {since}",
"sourceOffline": "Sin conexión",
+ "sourceCollapse": "Contraer",
+ "sourceExpand": "Expandir",
"stateQueued": "En cola",
"stateRunning": "En ejecución",
"stateWaitingApproval": "Esperando aprobación",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Métricas",
"drawerResult": "Resultado",
"drawerActions": "Acciones",
+ "drawerClose": "Cerrar",
+ "copyTrace": "Copiar traza JSON",
"actionApprove": "Aprobar plan",
"actionCancel": "Cancelar",
"actionSeeInGraph": "Ver en el gráfico",
"actionDone": "Acción aplicada",
"actionFailed": "Error en la acción: {error}",
+ "detailFailed": "No se pudieron cargar los detalles: {error}",
"mirroredInA2A": "Reflejado en A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json
index 2a256b9f45f..6b1c59f5cb3 100644
--- a/src/i18n/messages/fa.json
+++ b/src/i18n/messages/fa.json
@@ -13935,11 +13935,18 @@
"tabRouting": "مسیریابی",
"tabOverview": "نمای کلی",
"showCompleted": "نمایش تکمیلشدهها",
+ "searchPlaceholder": "جستجوی وظایف…",
+ "filterStates": "وضعیتها",
+ "filterSources": "منابع",
+ "filterProviders": "ارائهدهندگان",
+ "clearFilters": "پاک کردن فیلترها",
"sourceCloudAgent": "ایجنت ابری",
"sourceA2A": "A2A",
"sourceConductor": "هدایتگر",
"sourceStale": "منسوخ از {since}",
"sourceOffline": "آفلاین",
+ "sourceCollapse": "جمع کردن",
+ "sourceExpand": "باز کردن",
"stateQueued": "در صف",
"stateRunning": "در حال اجرا",
"stateWaitingApproval": "در انتظار تأیید",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "معیارها",
"drawerResult": "نتیجه",
"drawerActions": "اقدامات",
+ "drawerClose": "بستن",
+ "copyTrace": "کپی ردیابی JSON",
"actionApprove": "تأیید طرح",
"actionCancel": "لغو",
"actionSeeInGraph": "مشاهده در نمودار",
"actionDone": "اقدام اعمال شد",
"actionFailed": "اقدام ناموفق بود: {error}",
+ "detailFailed": "بارگذاری جزئیات ناموفق بود: {error}",
"mirroredInA2A": "بازتابیافته در A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json
index a31c9d8d22a..88a45e500ca 100644
--- a/src/i18n/messages/fi.json
+++ b/src/i18n/messages/fi.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Reititys",
"tabOverview": "Yleiskatsaus",
"showCompleted": "Näytä valmiit",
+ "searchPlaceholder": "Hae tehtäviä…",
+ "filterStates": "Tilat",
+ "filterSources": "Lähteet",
+ "filterProviders": "Palveluntarjoajat",
+ "clearFilters": "Tyhjennä suodattimet",
"sourceCloudAgent": "Pilviagentti",
"sourceA2A": "A2A",
"sourceConductor": "Konduktori",
"sourceStale": "Vanhentunut alkaen {since}",
"sourceOffline": "Offline-tilassa",
+ "sourceCollapse": "Pienennä",
+ "sourceExpand": "Laajenna",
"stateQueued": "Jonossa",
"stateRunning": "Käynnissä",
"stateWaitingApproval": "Odottaa hyväksyntää",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Mittarit",
"drawerResult": "Tulos",
"drawerActions": "Toiminnot",
+ "drawerClose": "Sulje",
+ "copyTrace": "Kopioi jäljitys-JSON",
"actionApprove": "Hyväksy suunnitelma",
"actionCancel": "Peruuta",
"actionSeeInGraph": "Näytä kaaviossa",
"actionDone": "Toiminto suoritettu",
"actionFailed": "Toiminto epäonnistui: {error}",
+ "detailFailed": "Tietojen lataus epäonnistui: {error}",
"mirroredInA2A": "Peilattu A2A:ssa"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index c06cd1f9835..19dab1efca5 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routage",
"tabOverview": "Vue d'ensemble",
"showCompleted": "Afficher les terminés",
+ "searchPlaceholder": "Rechercher des tâches…",
+ "filterStates": "États",
+ "filterSources": "Sources",
+ "filterProviders": "Fournisseurs",
+ "clearFilters": "Effacer les filtres",
"sourceCloudAgent": "Agent cloud",
"sourceA2A": "A2A",
"sourceConductor": "Conducteur",
"sourceStale": "Obsolète depuis {since}",
"sourceOffline": "Hors ligne",
+ "sourceCollapse": "Réduire",
+ "sourceExpand": "Développer",
"stateQueued": "En file d'attente",
"stateRunning": "En cours",
"stateWaitingApproval": "En attente d'approbation",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Métriques",
"drawerResult": "Résultat",
"drawerActions": "Actions",
+ "drawerClose": "Fermer",
+ "copyTrace": "Copier la trace JSON",
"actionApprove": "Approuver le plan",
"actionCancel": "Annuler",
"actionSeeInGraph": "Voir dans le graphe",
"actionDone": "Action appliquée",
"actionFailed": "Échec de l'action : {error}",
+ "detailFailed": "Échec du chargement des détails : {error}",
"mirroredInA2A": "Reflété dans A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json
index 439a71e4204..b20437b2a4e 100644
--- a/src/i18n/messages/gu.json
+++ b/src/i18n/messages/gu.json
@@ -13935,11 +13935,18 @@
"tabRouting": "રાઉટિંગ",
"tabOverview": "ઝાંખી",
"showCompleted": "પૂર્ણ થયેલા બતાવો",
+ "searchPlaceholder": "કાર્યો શોધો…",
+ "filterStates": "સ્થિતિઓ",
+ "filterSources": "સ્રોતો",
+ "filterProviders": "પ્રદાતાઓ",
+ "clearFilters": "ફિલ્ટર્સ સાફ કરો",
"sourceCloudAgent": "ક્લાઉડ એજન્ટ",
"sourceA2A": "A2A",
"sourceConductor": "સંચાલક",
"sourceStale": "{since} થી જૂનું",
"sourceOffline": "ઑફલાઇન",
+ "sourceCollapse": "સંકુચિત કરો",
+ "sourceExpand": "વિસ્તૃત કરો",
"stateQueued": "કતારમાં",
"stateRunning": "ચાલી રહ્યું છે",
"stateWaitingApproval": "મંજૂરીની રાહ જોઈ રહ્યું છે",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "મેટ્રિક્સ",
"drawerResult": "પરિણામ",
"drawerActions": "ક્રિયાઓ",
+ "drawerClose": "બંધ કરો",
+ "copyTrace": "ટ્રેસ JSON કૉપિ કરો",
"actionApprove": "યોજના મંજૂર કરો",
"actionCancel": "રદ કરો",
"actionSeeInGraph": "ગ્રાફમાં જુઓ",
"actionDone": "ક્રિયા લાગુ કરાઈ",
"actionFailed": "ક્રિયા નિષ્ફળ: {error}",
+ "detailFailed": "વિગતો લોડ કરવામાં નિષ્ફળ: {error}",
"mirroredInA2A": "A2A માં પ્રતિબિંબિત"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json
index fd5329cd56c..e3702ef32e7 100644
--- a/src/i18n/messages/he.json
+++ b/src/i18n/messages/he.json
@@ -13935,11 +13935,18 @@
"tabRouting": "ניתוב",
"tabOverview": "סקירה כללית",
"showCompleted": "הצג הושלמו",
+ "searchPlaceholder": "חיפוש משימות…",
+ "filterStates": "מצבים",
+ "filterSources": "מקורות",
+ "filterProviders": "ספקים",
+ "clearFilters": "ניקוי מסננים",
"sourceCloudAgent": "סוכן ענן",
"sourceA2A": "A2A",
"sourceConductor": "מנחה",
"sourceStale": "לא עדכני מאז {since}",
"sourceOffline": "לא מקוון",
+ "sourceCollapse": "כיווץ",
+ "sourceExpand": "הרחבה",
"stateQueued": "בתור",
"stateRunning": "פועל",
"stateWaitingApproval": "ממתין לאישור",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "מדדים",
"drawerResult": "תוצאה",
"drawerActions": "פעולות",
+ "drawerClose": "סגירה",
+ "copyTrace": "העתקת מעקב JSON",
"actionApprove": "אשר תוכנית",
"actionCancel": "בטל",
"actionSeeInGraph": "הצג בגרף",
"actionDone": "הפעולה בוצעה",
"actionFailed": "הפעולה נכשלה: {error}",
+ "detailFailed": "טעינת הפרטים נכשלה: {error}",
"mirroredInA2A": "משוקף ב-A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json
index 59528d6a017..818e449c1c3 100644
--- a/src/i18n/messages/hi.json
+++ b/src/i18n/messages/hi.json
@@ -13935,11 +13935,18 @@
"tabRouting": "रूटिंग",
"tabOverview": "अवलोकन",
"showCompleted": "पूर्ण दिखाएं",
+ "searchPlaceholder": "कार्य खोजें…",
+ "filterStates": "स्थितियाँ",
+ "filterSources": "स्रोत",
+ "filterProviders": "प्रदाता",
+ "clearFilters": "फ़िल्टर साफ़ करें",
"sourceCloudAgent": "क्लाउड एजेंट",
"sourceA2A": "A2A",
"sourceConductor": "संवहनकर्ता",
"sourceStale": "{since} से पुराना",
"sourceOffline": "ऑफ़लाइन",
+ "sourceCollapse": "संक्षिप्त करें",
+ "sourceExpand": "विस्तृत करें",
"stateQueued": "कतार में",
"stateRunning": "चल रहा है",
"stateWaitingApproval": "स्वीकृति की प्रतीक्षा में",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "मेट्रिक्स",
"drawerResult": "परिणाम",
"drawerActions": "कार्रवाइयां",
+ "drawerClose": "बंद करें",
+ "copyTrace": "ट्रेस JSON कॉपी करें",
"actionApprove": "योजना स्वीकृत करें",
"actionCancel": "रद्द करें",
"actionSeeInGraph": "ग्राफ़ में देखें",
"actionDone": "कार्रवाई लागू की गई",
"actionFailed": "कार्रवाई विफल: {error}",
+ "detailFailed": "विवरण लोड करने में विफल: {error}",
"mirroredInA2A": "A2A में प्रतिबिंबित"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json
index c8ddffece7f..eb8efd58ef0 100644
--- a/src/i18n/messages/hu.json
+++ b/src/i18n/messages/hu.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Útválasztás",
"tabOverview": "Áttekintés",
"showCompleted": "Befejezettek megjelenítése",
+ "searchPlaceholder": "Feladatok keresése…",
+ "filterStates": "Állapotok",
+ "filterSources": "Források",
+ "filterProviders": "Szolgáltatók",
+ "clearFilters": "Szűrők törlése",
"sourceCloudAgent": "Felhőügynök",
"sourceA2A": "A2A",
"sourceConductor": "Vezető",
"sourceStale": "Elavult ekkor óta: {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Összecsukás",
+ "sourceExpand": "Kibontás",
"stateQueued": "Sorban áll",
"stateRunning": "Fut",
"stateWaitingApproval": "Jóváhagyásra vár",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrikák",
"drawerResult": "Eredmény",
"drawerActions": "Műveletek",
+ "drawerClose": "Bezárás",
+ "copyTrace": "Nyomkövetési JSON másolása",
"actionApprove": "Terv jóváhagyása",
"actionCancel": "Mégse",
"actionSeeInGraph": "Megtekintés a grafikonon",
"actionDone": "Művelet végrehajtva",
"actionFailed": "A művelet sikertelen: {error}",
+ "detailFailed": "A részletek betöltése sikertelen: {error}",
"mirroredInA2A": "Tükrözve az A2A-ban"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json
index a627c88cc64..a42bb31f6a5 100644
--- a/src/i18n/messages/id.json
+++ b/src/i18n/messages/id.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Perutean",
"tabOverview": "Ikhtisar",
"showCompleted": "Tampilkan yang selesai",
+ "searchPlaceholder": "Cari tugas…",
+ "filterStates": "Status",
+ "filterSources": "Sumber",
+ "filterProviders": "Penyedia",
+ "clearFilters": "Hapus filter",
"sourceCloudAgent": "Agen Cloud",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Kedaluwarsa sejak {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Ciutkan",
+ "sourceExpand": "Perluas",
"stateQueued": "Dalam antrean",
"stateRunning": "Berjalan",
"stateWaitingApproval": "Menunggu persetujuan",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrik",
"drawerResult": "Hasil",
"drawerActions": "Tindakan",
+ "drawerClose": "Tutup",
+ "copyTrace": "Salin trace JSON",
"actionApprove": "Setujui rencana",
"actionCancel": "Batal",
"actionSeeInGraph": "Lihat di grafik",
"actionDone": "Tindakan diterapkan",
"actionFailed": "Tindakan gagal: {error}",
+ "detailFailed": "Gagal memuat detail: {error}",
"mirroredInA2A": "Dicerminkan di A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json
index 688c4e4b71d..a84a2f59ea4 100644
--- a/src/i18n/messages/in.json
+++ b/src/i18n/messages/in.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Perutean",
"tabOverview": "Ikhtisar",
"showCompleted": "Tampilkan yang selesai",
+ "searchPlaceholder": "Cari tugas…",
+ "filterStates": "Status",
+ "filterSources": "Sumber",
+ "filterProviders": "Penyedia",
+ "clearFilters": "Hapus filter",
"sourceCloudAgent": "Agen Cloud",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Kedaluwarsa sejak {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Ciutkan",
+ "sourceExpand": "Perluas",
"stateQueued": "Dalam antrean",
"stateRunning": "Berjalan",
"stateWaitingApproval": "Menunggu persetujuan",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrik",
"drawerResult": "Hasil",
"drawerActions": "Tindakan",
+ "drawerClose": "Tutup",
+ "copyTrace": "Salin trace JSON",
"actionApprove": "Setujui rencana",
"actionCancel": "Batal",
"actionSeeInGraph": "Lihat di grafik",
"actionDone": "Tindakan diterapkan",
"actionFailed": "Tindakan gagal: {error}",
+ "detailFailed": "Gagal memuat detail: {error}",
"mirroredInA2A": "Dicerminkan di A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json
index 26564386041..88748dd4920 100644
--- a/src/i18n/messages/it.json
+++ b/src/i18n/messages/it.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routing",
"tabOverview": "Panoramica",
"showCompleted": "Mostra completati",
+ "searchPlaceholder": "Cerca attività…",
+ "filterStates": "Stati",
+ "filterSources": "Origini",
+ "filterProviders": "Fornitori",
+ "clearFilters": "Cancella filtri",
"sourceCloudAgent": "Agente cloud",
"sourceA2A": "A2A",
"sourceConductor": "Conduttore",
"sourceStale": "Non aggiornato dal {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Comprimi",
+ "sourceExpand": "Espandi",
"stateQueued": "In coda",
"stateRunning": "In esecuzione",
"stateWaitingApproval": "In attesa di approvazione",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metriche",
"drawerResult": "Risultato",
"drawerActions": "Azioni",
+ "drawerClose": "Chiudi",
+ "copyTrace": "Copia trace JSON",
"actionApprove": "Approva piano",
"actionCancel": "Annulla",
"actionSeeInGraph": "Visualizza nel grafico",
"actionDone": "Azione applicata",
"actionFailed": "Azione non riuscita: {error}",
+ "detailFailed": "Impossibile caricare i dettagli: {error}",
"mirroredInA2A": "Rispecchiato in A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index 52b0510bf26..daa5041fd6b 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -13935,11 +13935,18 @@
"tabRouting": "ルーティング",
"tabOverview": "概要",
"showCompleted": "完了を表示",
+ "searchPlaceholder": "タスクを検索…",
+ "filterStates": "状態",
+ "filterSources": "ソース",
+ "filterProviders": "プロバイダー",
+ "clearFilters": "フィルターをクリア",
"sourceCloudAgent": "クラウドエージェント",
"sourceA2A": "A2A",
"sourceConductor": "指揮者",
"sourceStale": "{since}から更新なし",
"sourceOffline": "オフライン",
+ "sourceCollapse": "折りたたむ",
+ "sourceExpand": "展開する",
"stateQueued": "キュー待ち",
"stateRunning": "実行中",
"stateWaitingApproval": "承認待ち",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "メトリクス",
"drawerResult": "結果",
"drawerActions": "アクション",
+ "drawerClose": "閉じる",
+ "copyTrace": "トレースJSONをコピー",
"actionApprove": "プランを承認",
"actionCancel": "キャンセル",
"actionSeeInGraph": "グラフで表示",
"actionDone": "アクションを適用しました",
"actionFailed": "アクションが失敗しました: {error}",
+ "detailFailed": "詳細の読み込みに失敗しました: {error}",
"mirroredInA2A": "A2Aにミラーリング"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index ab1e255363a..5a95e797ee3 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -13935,11 +13935,18 @@
"tabRouting": "라우팅",
"tabOverview": "개요",
"showCompleted": "완료 항목 표시",
+ "searchPlaceholder": "작업 검색…",
+ "filterStates": "상태",
+ "filterSources": "소스",
+ "filterProviders": "공급자",
+ "clearFilters": "필터 지우기",
"sourceCloudAgent": "클라우드 에이전트",
"sourceA2A": "A2A",
"sourceConductor": "지휘자",
"sourceStale": "{since}부터 오래됨",
"sourceOffline": "오프라인",
+ "sourceCollapse": "접기",
+ "sourceExpand": "펼치기",
"stateQueued": "대기 중",
"stateRunning": "실행 중",
"stateWaitingApproval": "승인 대기 중",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "지표",
"drawerResult": "결과",
"drawerActions": "작업",
+ "drawerClose": "닫기",
+ "copyTrace": "추적 JSON 복사",
"actionApprove": "계획 승인",
"actionCancel": "취소",
"actionSeeInGraph": "그래프에서 보기",
"actionDone": "작업이 적용됨",
"actionFailed": "작업 실패: {error}",
+ "detailFailed": "세부정보를 불러오지 못했습니다: {error}",
"mirroredInA2A": "A2A에 미러링됨"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json
index cd797308887..40e61f6f06d 100644
--- a/src/i18n/messages/mr.json
+++ b/src/i18n/messages/mr.json
@@ -13935,11 +13935,18 @@
"tabRouting": "राउटिंग",
"tabOverview": "आढावा",
"showCompleted": "पूर्ण झालेले दाखवा",
+ "searchPlaceholder": "कार्ये शोधा…",
+ "filterStates": "स्थिती",
+ "filterSources": "स्रोत",
+ "filterProviders": "प्रदाता",
+ "clearFilters": "फिल्टर साफ करा",
"sourceCloudAgent": "क्लाउड एजंट",
"sourceA2A": "A2A",
"sourceConductor": "संवहनकर्ता",
"sourceStale": "{since} पासून जुने",
"sourceOffline": "ऑफलाइन",
+ "sourceCollapse": "संकुचित करा",
+ "sourceExpand": "विस्तृत करा",
"stateQueued": "रांगेत",
"stateRunning": "सुरू आहे",
"stateWaitingApproval": "मंजुरीची वाट पाहत आहे",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "मेट्रिक्स",
"drawerResult": "निकाल",
"drawerActions": "क्रिया",
+ "drawerClose": "बंद करा",
+ "copyTrace": "ट्रेस JSON कॉपी करा",
"actionApprove": "योजना मंजूर करा",
"actionCancel": "रद्द करा",
"actionSeeInGraph": "आलेखात पहा",
"actionDone": "क्रिया लागू केली",
"actionFailed": "क्रिया अयशस्वी: {error}",
+ "detailFailed": "तपशील लोड करण्यात अयशस्वी: {error}",
"mirroredInA2A": "A2A मध्ये प्रतिबिंबित"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json
index 7935db042dd..87d11ce69e5 100644
--- a/src/i18n/messages/ms.json
+++ b/src/i18n/messages/ms.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Penghalaan",
"tabOverview": "Gambaran Keseluruhan",
"showCompleted": "Tunjukkan yang selesai",
+ "searchPlaceholder": "Cari tugas…",
+ "filterStates": "Status",
+ "filterSources": "Sumber",
+ "filterProviders": "Pembekal",
+ "clearFilters": "Kosongkan penapis",
"sourceCloudAgent": "Ejen Awan",
"sourceA2A": "A2A",
"sourceConductor": "Pengendali",
"sourceStale": "Lapuk sejak {since}",
"sourceOffline": "Luar talian",
+ "sourceCollapse": "Kuncupkan",
+ "sourceExpand": "Kembangkan",
"stateQueued": "Dalam giliran",
"stateRunning": "Berjalan",
"stateWaitingApproval": "Menunggu kelulusan",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrik",
"drawerResult": "Keputusan",
"drawerActions": "Tindakan",
+ "drawerClose": "Tutup",
+ "copyTrace": "Salin JSON jejak",
"actionApprove": "Luluskan pelan",
"actionCancel": "Batal",
"actionSeeInGraph": "Lihat dalam graf",
"actionDone": "Tindakan digunakan",
"actionFailed": "Tindakan gagal: {error}",
+ "detailFailed": "Gagal memuatkan butiran: {error}",
"mirroredInA2A": "Dicerminkan dalam A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json
index aa8228ea9aa..116d61ff25b 100644
--- a/src/i18n/messages/nl.json
+++ b/src/i18n/messages/nl.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routering",
"tabOverview": "Overzicht",
"showCompleted": "Voltooide items tonen",
+ "searchPlaceholder": "Taken zoeken…",
+ "filterStates": "Statussen",
+ "filterSources": "Bronnen",
+ "filterProviders": "Aanbieders",
+ "clearFilters": "Filters wissen",
"sourceCloudAgent": "Cloud Agent",
"sourceA2A": "A2A",
"sourceConductor": "Conductor",
"sourceStale": "Verouderd sinds {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Inklappen",
+ "sourceExpand": "Uitklappen",
"stateQueued": "In wachtrij",
"stateRunning": "Actief",
"stateWaitingApproval": "Wacht op goedkeuring",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Statistieken",
"drawerResult": "Resultaat",
"drawerActions": "Acties",
+ "drawerClose": "Sluiten",
+ "copyTrace": "Trace-JSON kopiëren",
"actionApprove": "Plan goedkeuren",
"actionCancel": "Annuleren",
"actionSeeInGraph": "Bekijk in grafiek",
"actionDone": "Actie toegepast",
"actionFailed": "Actie mislukt: {error}",
+ "detailFailed": "Details laden mislukt: {error}",
"mirroredInA2A": "Weergegeven in A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json
index d75c7f1bf0e..02732fee099 100644
--- a/src/i18n/messages/no.json
+++ b/src/i18n/messages/no.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Ruting",
"tabOverview": "Oversikt",
"showCompleted": "Vis fullførte",
+ "searchPlaceholder": "Søk i oppgaver…",
+ "filterStates": "Tilstander",
+ "filterSources": "Kilder",
+ "filterProviders": "Leverandører",
+ "clearFilters": "Fjern filtre",
"sourceCloudAgent": "Skyagent",
"sourceA2A": "A2A",
"sourceConductor": "Konduktør",
"sourceStale": "Utdatert siden {since}",
"sourceOffline": "Frakoblet",
+ "sourceCollapse": "Fold sammen",
+ "sourceExpand": "Fold ut",
"stateQueued": "I kø",
"stateRunning": "Kjører",
"stateWaitingApproval": "Venter på godkjenning",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Målinger",
"drawerResult": "Resultat",
"drawerActions": "Handlinger",
+ "drawerClose": "Lukk",
+ "copyTrace": "Kopiér spor (JSON)",
"actionApprove": "Godkjenn plan",
"actionCancel": "Avbryt",
"actionSeeInGraph": "Se i grafen",
"actionDone": "Handling utført",
"actionFailed": "Handling mislyktes: {error}",
+ "detailFailed": "Kunne ikke laste inn detaljer: {error}",
"mirroredInA2A": "Speilet i A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json
index bcbd969b122..6d15e78d9f3 100644
--- a/src/i18n/messages/phi.json
+++ b/src/i18n/messages/phi.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routing",
"tabOverview": "Pangkalahatang-ideya",
"showCompleted": "Ipakita ang mga natapos na",
+ "searchPlaceholder": "Maghanap ng mga gawain…",
+ "filterStates": "Mga Katayuan",
+ "filterSources": "Mga Pinagmulan",
+ "filterProviders": "Mga Provider",
+ "clearFilters": "I-clear ang mga filter",
"sourceCloudAgent": "Cloud Agent",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Luma na simula {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "I-collapse",
+ "sourceExpand": "I-expand",
"stateQueued": "Nakapila",
"stateRunning": "Tumatakbo",
"stateWaitingApproval": "Naghihintay ng Pag-apruba",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Mga Sukatan",
"drawerResult": "Resulta",
"drawerActions": "Mga Aksyon",
+ "drawerClose": "Isara",
+ "copyTrace": "Kopyahin ang trace JSON",
"actionApprove": "Aprubahan ang plano",
"actionCancel": "Kanselahin",
"actionSeeInGraph": "Tingnan sa graph",
"actionDone": "Naisagawa ang aksyon",
"actionFailed": "Nabigo ang aksyon: {error}",
+ "detailFailed": "Nabigong i-load ang mga detalye: {error}",
"mirroredInA2A": "Naka-mirror sa A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json
index 0fee784ce9b..37a3bb82881 100644
--- a/src/i18n/messages/pl.json
+++ b/src/i18n/messages/pl.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routing",
"tabOverview": "Przegląd",
"showCompleted": "Pokaż ukończone",
+ "searchPlaceholder": "Szukaj zadań…",
+ "filterStates": "Stany",
+ "filterSources": "Źródła",
+ "filterProviders": "Dostawcy",
+ "clearFilters": "Wyczyść filtry",
"sourceCloudAgent": "Agent chmurowy",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Nieaktualne od {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Zwiń",
+ "sourceExpand": "Rozwiń",
"stateQueued": "W kolejce",
"stateRunning": "Uruchomiony",
"stateWaitingApproval": "Oczekuje na zatwierdzenie",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metryki",
"drawerResult": "Wynik",
"drawerActions": "Akcje",
+ "drawerClose": "Zamknij",
+ "copyTrace": "Kopiuj JSON śladu",
"actionApprove": "Zatwierdź plan",
"actionCancel": "Anuluj",
"actionSeeInGraph": "Zobacz na grafie",
"actionDone": "Akcja wykonana",
"actionFailed": "Akcja nie powiodła się: {error}",
+ "detailFailed": "Nie udało się wczytać szczegółów: {error}",
"mirroredInA2A": "Odzwierciedlone w A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json
index df8baf6d4c2..ea713d2d9b9 100644
--- a/src/i18n/messages/pt-BR.json
+++ b/src/i18n/messages/pt-BR.json
@@ -13943,11 +13943,18 @@
"tabRouting": "Roteamento",
"tabOverview": "Visão geral",
"showCompleted": "Mostrar concluídos",
+ "searchPlaceholder": "Buscar tarefas…",
+ "filterStates": "Estados",
+ "filterSources": "Origens",
+ "filterProviders": "Provedores",
+ "clearFilters": "Limpar filtros",
"sourceCloudAgent": "Cloud Agent",
"sourceA2A": "A2A",
"sourceConductor": "Conductor",
"sourceStale": "Sem atualização desde {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Recolher",
+ "sourceExpand": "Expandir",
"stateQueued": "Na fila",
"stateRunning": "Em execução",
"stateWaitingApproval": "Aguardando aprovação",
@@ -13964,11 +13971,14 @@
"drawerMetrics": "Métricas",
"drawerResult": "Resultado",
"drawerActions": "Ações",
+ "drawerClose": "Fechar",
+ "copyTrace": "Copiar trace em JSON",
"actionApprove": "Aprovar plano",
"actionCancel": "Cancelar",
"actionSeeInGraph": "Ver no grafo",
"actionDone": "Ação aplicada",
"actionFailed": "Ação falhou: {error}",
+ "detailFailed": "Falha ao carregar detalhes: {error}",
"mirroredInA2A": "Espelhado no A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index b2dc76d48af..98c9123e7bb 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Encaminhamento",
"tabOverview": "Visão Geral",
"showCompleted": "Mostrar concluídos",
+ "searchPlaceholder": "Pesquisar tarefas…",
+ "filterStates": "Estados",
+ "filterSources": "Origens",
+ "filterProviders": "Provedores",
+ "clearFilters": "Limpar filtros",
"sourceCloudAgent": "Agente na Nuvem",
"sourceA2A": "A2A",
"sourceConductor": "Condutor",
"sourceStale": "Desatualizado desde {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Recolher",
+ "sourceExpand": "Expandir",
"stateQueued": "Em fila",
"stateRunning": "Em execução",
"stateWaitingApproval": "A aguardar aprovação",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Métricas",
"drawerResult": "Resultado",
"drawerActions": "Ações",
+ "drawerClose": "Fechar",
+ "copyTrace": "Copiar trace em JSON",
"actionApprove": "Aprovar plano",
"actionCancel": "Cancelar",
"actionSeeInGraph": "Ver no grafo",
"actionDone": "Ação aplicada",
"actionFailed": "Falha na ação: {error}",
+ "detailFailed": "Falha ao carregar detalhes: {error}",
"mirroredInA2A": "Espelhado no A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json
index c501a42e062..75b1d13a765 100644
--- a/src/i18n/messages/ro.json
+++ b/src/i18n/messages/ro.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Rutare",
"tabOverview": "Prezentare generală",
"showCompleted": "Afișează cele finalizate",
+ "searchPlaceholder": "Caută sarcini…",
+ "filterStates": "Stări",
+ "filterSources": "Surse",
+ "filterProviders": "Furnizori",
+ "clearFilters": "Golește filtrele",
"sourceCloudAgent": "Agent Cloud",
"sourceA2A": "A2A",
"sourceConductor": "Conductor",
"sourceStale": "Perimat din {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Restrânge",
+ "sourceExpand": "Extinde",
"stateQueued": "În așteptare",
"stateRunning": "În execuție",
"stateWaitingApproval": "Așteaptă aprobare",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrici",
"drawerResult": "Rezultat",
"drawerActions": "Acțiuni",
+ "drawerClose": "Închide",
+ "copyTrace": "Copiază JSON-ul de urmărire",
"actionApprove": "Aprobă planul",
"actionCancel": "Anulează",
"actionSeeInGraph": "Vezi în grafic",
"actionDone": "Acțiune aplicată",
"actionFailed": "Acțiune eșuată: {error}",
+ "detailFailed": "Încărcarea detaliilor a eșuat: {error}",
"mirroredInA2A": "Reflectat în A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json
index 9bdea2f86e4..cf7ccd6e6b6 100644
--- a/src/i18n/messages/ru.json
+++ b/src/i18n/messages/ru.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Маршрутизация",
"tabOverview": "Обзор",
"showCompleted": "Показать завершённые",
+ "searchPlaceholder": "Поиск задач…",
+ "filterStates": "Состояния",
+ "filterSources": "Источники",
+ "filterProviders": "Провайдеры",
+ "clearFilters": "Сбросить фильтры",
"sourceCloudAgent": "Облачный агент",
"sourceA2A": "A2A",
"sourceConductor": "Кондуктор",
"sourceStale": "Устарело с {since}",
"sourceOffline": "Не в сети",
+ "sourceCollapse": "Свернуть",
+ "sourceExpand": "Развернуть",
"stateQueued": "В очереди",
"stateRunning": "Выполняется",
"stateWaitingApproval": "Ожидает подтверждения",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Метрики",
"drawerResult": "Результат",
"drawerActions": "Действия",
+ "drawerClose": "Закрыть",
+ "copyTrace": "Скопировать трассировку JSON",
"actionApprove": "Утвердить план",
"actionCancel": "Отмена",
"actionSeeInGraph": "Посмотреть на графе",
"actionDone": "Действие применено",
"actionFailed": "Действие не удалось: {error}",
+ "detailFailed": "Не удалось загрузить детали: {error}",
"mirroredInA2A": "Отражено в A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json
index ced4e9ae740..254eb1c471b 100644
--- a/src/i18n/messages/sk.json
+++ b/src/i18n/messages/sk.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Smerovanie",
"tabOverview": "Prehľad",
"showCompleted": "Zobraziť dokončené",
+ "searchPlaceholder": "Hľadať úlohy…",
+ "filterStates": "Stavy",
+ "filterSources": "Zdroje",
+ "filterProviders": "Poskytovatelia",
+ "clearFilters": "Vymazať filtre",
"sourceCloudAgent": "Cloudový agent",
"sourceA2A": "A2A",
"sourceConductor": "Konduktor",
"sourceStale": "Zastarané od {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Zbaliť",
+ "sourceExpand": "Rozbaliť",
"stateQueued": "V rade",
"stateRunning": "Beží",
"stateWaitingApproval": "Čaká na schválenie",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metriky",
"drawerResult": "Výsledok",
"drawerActions": "Akcie",
+ "drawerClose": "Zavrieť",
+ "copyTrace": "Kopírovať trasovanie JSON",
"actionApprove": "Schváliť plán",
"actionCancel": "Zrušiť",
"actionSeeInGraph": "Zobraziť v grafe",
"actionDone": "Akcia použitá",
"actionFailed": "Akcia zlyhala: {error}",
+ "detailFailed": "Nepodarilo sa načítať podrobnosti: {error}",
"mirroredInA2A": "Zrkadlené v A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json
index f42dc76f6e6..ce3a61077be 100644
--- a/src/i18n/messages/sv.json
+++ b/src/i18n/messages/sv.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Routing",
"tabOverview": "Översikt",
"showCompleted": "Visa slutförda",
+ "searchPlaceholder": "Sök uppgifter…",
+ "filterStates": "Tillstånd",
+ "filterSources": "Källor",
+ "filterProviders": "Leverantörer",
+ "clearFilters": "Rensa filter",
"sourceCloudAgent": "Molnagent",
"sourceA2A": "A2A",
"sourceConductor": "Ledare",
"sourceStale": "Föråldrad sedan {since}",
"sourceOffline": "Offline",
+ "sourceCollapse": "Fäll ihop",
+ "sourceExpand": "Fäll ut",
"stateQueued": "Köad",
"stateRunning": "Körs",
"stateWaitingApproval": "Väntar på godkännande",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Mätvärden",
"drawerResult": "Resultat",
"drawerActions": "Åtgärder",
+ "drawerClose": "Stäng",
+ "copyTrace": "Kopiera spårnings-JSON",
"actionApprove": "Godkänn plan",
"actionCancel": "Avbryt",
"actionSeeInGraph": "Visa i grafen",
"actionDone": "Åtgärd tillämpad",
"actionFailed": "Åtgärden misslyckades: {error}",
+ "detailFailed": "Det gick inte att läsa in detaljer: {error}",
"mirroredInA2A": "Speglad i A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json
index f777e7e65fa..98493aed96b 100644
--- a/src/i18n/messages/sw.json
+++ b/src/i18n/messages/sw.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Uelekezaji",
"tabOverview": "Muhtasari",
"showCompleted": "Onyesha vilivyokamilika",
+ "searchPlaceholder": "Tafuta kazi…",
+ "filterStates": "Hali",
+ "filterSources": "Vyanzo",
+ "filterProviders": "Watoa huduma",
+ "clearFilters": "Futa vichujio",
"sourceCloudAgent": "Wakala wa Wingu",
"sourceA2A": "A2A",
"sourceConductor": "Msimamizi",
"sourceStale": "Imepitwa na wakati tangu {since}",
"sourceOffline": "Nje ya mtandao",
+ "sourceCollapse": "Kunja",
+ "sourceExpand": "Panua",
"stateQueued": "Kwenye foleni",
"stateRunning": "Inaendesha",
"stateWaitingApproval": "Inasubiri idhini",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Vipimo",
"drawerResult": "Matokeo",
"drawerActions": "Vitendo",
+ "drawerClose": "Funga",
+ "copyTrace": "Nakili JSON ya ufuatiliaji",
"actionApprove": "Idhinisha mpango",
"actionCancel": "Ghairi",
"actionSeeInGraph": "Ona kwenye grafu",
"actionDone": "Kitendo kimetumika",
"actionFailed": "Kitendo kimeshindwa: {error}",
+ "detailFailed": "Imeshindwa kupakia maelezo: {error}",
"mirroredInA2A": "Kimeakisiwa katika A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json
index f7b4d3e6822..4c25ed53077 100644
--- a/src/i18n/messages/ta.json
+++ b/src/i18n/messages/ta.json
@@ -13935,11 +13935,18 @@
"tabRouting": "ரூட்டிங்",
"tabOverview": "மேலோட்டம்",
"showCompleted": "முடிந்தவற்றைக் காட்டு",
+ "searchPlaceholder": "பணிகளைத் தேடு…",
+ "filterStates": "நிலைகள்",
+ "filterSources": "மூலங்கள்",
+ "filterProviders": "வழங்குநர்கள்",
+ "clearFilters": "வடிப்பான்களை அழி",
"sourceCloudAgent": "கிளவுட் ஏஜென்ட்",
"sourceA2A": "A2A",
"sourceConductor": "கட்டுப்படுத்தி",
"sourceStale": "{since} முதல் பழையது",
"sourceOffline": "ஆஃப்லைன்",
+ "sourceCollapse": "சுருக்கு",
+ "sourceExpand": "விரிவாக்கு",
"stateQueued": "வரிசையில்",
"stateRunning": "இயங்குகிறது",
"stateWaitingApproval": "ஒப்புதலுக்காகக் காத்திருக்கிறது",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "அளவீடுகள்",
"drawerResult": "முடிவு",
"drawerActions": "செயல்கள்",
+ "drawerClose": "மூடு",
+ "copyTrace": "ட்ரேஸ் JSON-ஐ நகலெடு",
"actionApprove": "திட்டத்தை ஒப்புதல் அளி",
"actionCancel": "ரத்துசெய்",
"actionSeeInGraph": "வரைபடத்தில் பார்",
"actionDone": "செயல் பயன்படுத்தப்பட்டது",
"actionFailed": "செயல் தோல்வியடைந்தது: {error}",
+ "detailFailed": "விவரங்களை ஏற்ற முடியவில்லை: {error}",
"mirroredInA2A": "A2A இல் பிரதிபலிக்கப்பட்டது"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json
index 40b346360d7..05f9deccf7b 100644
--- a/src/i18n/messages/te.json
+++ b/src/i18n/messages/te.json
@@ -13935,11 +13935,18 @@
"tabRouting": "రూటింగ్",
"tabOverview": "అవలోకనం",
"showCompleted": "పూర్తయినవి చూపించు",
+ "searchPlaceholder": "పనులను వెతకండి…",
+ "filterStates": "స్థితులు",
+ "filterSources": "మూలాలు",
+ "filterProviders": "ప్రదాతలు",
+ "clearFilters": "ఫిల్టర్లను క్లియర్ చేయి",
"sourceCloudAgent": "క్లౌడ్ ఏజెంట్",
"sourceA2A": "A2A",
"sourceConductor": "కండక్టర్",
"sourceStale": "{since} నుండి పాతది",
"sourceOffline": "ఆఫ్లైన్",
+ "sourceCollapse": "కుదించు",
+ "sourceExpand": "విస్తరించు",
"stateQueued": "క్యూలో ఉంది",
"stateRunning": "నడుస్తోంది",
"stateWaitingApproval": "ఆమోదం కోసం వేచి ఉంది",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "మెట్రిక్స్",
"drawerResult": "ఫలితం",
"drawerActions": "చర్యలు",
+ "drawerClose": "మూసివేయి",
+ "copyTrace": "ట్రేస్ JSON కాపీ చేయి",
"actionApprove": "ప్రణాళికను ఆమోదించండి",
"actionCancel": "రద్దు చేయండి",
"actionSeeInGraph": "గ్రాఫ్లో చూడండి",
"actionDone": "చర్య వర్తింపజేయబడింది",
"actionFailed": "చర్య విఫలమైంది: {error}",
+ "detailFailed": "వివరాలను లోడ్ చేయడంలో విఫలమైంది: {error}",
"mirroredInA2A": "A2Aలో ప్రతిబింబించబడింది"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json
index 5d6451770ca..7b4deb50271 100644
--- a/src/i18n/messages/th.json
+++ b/src/i18n/messages/th.json
@@ -13935,11 +13935,18 @@
"tabRouting": "การกำหนดเส้นทาง",
"tabOverview": "ภาพรวม",
"showCompleted": "แสดงที่เสร็จสิ้นแล้ว",
+ "searchPlaceholder": "ค้นหางาน…",
+ "filterStates": "สถานะ",
+ "filterSources": "แหล่งที่มา",
+ "filterProviders": "ผู้ให้บริการ",
+ "clearFilters": "ล้างตัวกรอง",
"sourceCloudAgent": "เอเจนต์คลาวด์",
"sourceA2A": "A2A",
"sourceConductor": "ตัวนำ",
"sourceStale": "ไม่มีการอัปเดตตั้งแต่ {since}",
"sourceOffline": "ออฟไลน์",
+ "sourceCollapse": "ย่อ",
+ "sourceExpand": "ขยาย",
"stateQueued": "อยู่ในคิว",
"stateRunning": "กำลังทำงาน",
"stateWaitingApproval": "รอการอนุมัติ",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "เมตริก",
"drawerResult": "ผลลัพธ์",
"drawerActions": "การดำเนินการ",
+ "drawerClose": "ปิด",
+ "copyTrace": "คัดลอก JSON การติดตาม",
"actionApprove": "อนุมัติแผน",
"actionCancel": "ยกเลิก",
"actionSeeInGraph": "ดูในกราฟ",
"actionDone": "ดำเนินการเรียบร้อยแล้ว",
"actionFailed": "การดำเนินการล้มเหลว: {error}",
+ "detailFailed": "โหลดรายละเอียดไม่สำเร็จ: {error}",
"mirroredInA2A": "สะท้อนใน A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json
index c62cc8a0422..9aad233c382 100644
--- a/src/i18n/messages/tr.json
+++ b/src/i18n/messages/tr.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Yönlendirme",
"tabOverview": "Genel Bakış",
"showCompleted": "Tamamlananları göster",
+ "searchPlaceholder": "Görevlerde ara…",
+ "filterStates": "Durumlar",
+ "filterSources": "Kaynaklar",
+ "filterProviders": "Sağlayıcılar",
+ "clearFilters": "Filtreleri temizle",
"sourceCloudAgent": "Bulut Ajanı",
"sourceA2A": "A2A",
"sourceConductor": "İletken",
"sourceStale": "{since} tarihinden beri güncel değil",
"sourceOffline": "Çevrimdışı",
+ "sourceCollapse": "Daralt",
+ "sourceExpand": "Genişlet",
"stateQueued": "Sırada",
"stateRunning": "Çalışıyor",
"stateWaitingApproval": "Onay bekliyor",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Metrikler",
"drawerResult": "Sonuç",
"drawerActions": "Eylemler",
+ "drawerClose": "Kapat",
+ "copyTrace": "İzleme JSON'ını kopyala",
"actionApprove": "Planı onayla",
"actionCancel": "İptal et",
"actionSeeInGraph": "Grafikte gör",
"actionDone": "İşlem uygulandı",
"actionFailed": "İşlem başarısız: {error}",
+ "detailFailed": "Ayrıntılar yüklenemedi: {error}",
"mirroredInA2A": "A2A'da yansıtıldı"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json
index 452055bb79f..0c613b106d1 100644
--- a/src/i18n/messages/uk-UA.json
+++ b/src/i18n/messages/uk-UA.json
@@ -13935,11 +13935,18 @@
"tabRouting": "Маршрутизація",
"tabOverview": "Огляд",
"showCompleted": "Показати завершені",
+ "searchPlaceholder": "Пошук завдань…",
+ "filterStates": "Стани",
+ "filterSources": "Джерела",
+ "filterProviders": "Провайдери",
+ "clearFilters": "Очистити фільтри",
"sourceCloudAgent": "Хмарний агент",
"sourceA2A": "A2A",
"sourceConductor": "Кондуктор",
"sourceStale": "Немає оновлень з {since}",
"sourceOffline": "Офлайн",
+ "sourceCollapse": "Згорнути",
+ "sourceExpand": "Розгорнути",
"stateQueued": "У черзі",
"stateRunning": "Виконується",
"stateWaitingApproval": "Очікує затвердження",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "Метрики",
"drawerResult": "Результат",
"drawerActions": "Дії",
+ "drawerClose": "Закрити",
+ "copyTrace": "Копіювати трасування JSON",
"actionApprove": "Затвердити план",
"actionCancel": "Скасувати",
"actionSeeInGraph": "Переглянути на графі",
"actionDone": "Дію виконано",
"actionFailed": "Дія не виконана: {error}",
+ "detailFailed": "Не вдалося завантажити деталі: {error}",
"mirroredInA2A": "Відображено в A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json
index 8d9fc93d4a7..62b1900f1c9 100644
--- a/src/i18n/messages/ur.json
+++ b/src/i18n/messages/ur.json
@@ -13935,11 +13935,18 @@
"tabRouting": "روٹنگ",
"tabOverview": "جائزہ",
"showCompleted": "مکمل شدہ دکھائیں",
+ "searchPlaceholder": "کاموں کو تلاش کریں…",
+ "filterStates": "حالتیں",
+ "filterSources": "ذرائع",
+ "filterProviders": "فراہم کنندگان",
+ "clearFilters": "فلٹرز صاف کریں",
"sourceCloudAgent": "کلاؤڈ ایجنٹ",
"sourceA2A": "A2A",
"sourceConductor": "کنڈکٹر",
"sourceStale": "{since} سے تازہ نہیں",
"sourceOffline": "آف لائن",
+ "sourceCollapse": "سکیڑیں",
+ "sourceExpand": "پھیلائیں",
"stateQueued": "قطار میں",
"stateRunning": "چل رہا ہے",
"stateWaitingApproval": "منظوری کا منتظر",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "میٹرکس",
"drawerResult": "نتیجہ",
"drawerActions": "اقدامات",
+ "drawerClose": "بند کریں",
+ "copyTrace": "ٹریس JSON کاپی کریں",
"actionApprove": "منصوبہ منظور کریں",
"actionCancel": "منسوخ کریں",
"actionSeeInGraph": "گراف میں دیکھیں",
"actionDone": "اقدام لاگو ہو گیا",
"actionFailed": "اقدام ناکام: {error}",
+ "detailFailed": "تفصیلات لوڈ کرنے میں ناکامی: {error}",
"mirroredInA2A": "A2A میں مطابقت شدہ"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json
index 1c016d98d7b..513c547bdbd 100644
--- a/src/i18n/messages/vi.json
+++ b/src/i18n/messages/vi.json
@@ -13943,11 +13943,18 @@
"tabRouting": "Định tuyến",
"tabOverview": "Tổng quan",
"showCompleted": "Hiển thị đã hoàn tất",
+ "searchPlaceholder": "Tìm kiếm tác vụ…",
+ "filterStates": "Trạng thái",
+ "filterSources": "Nguồn",
+ "filterProviders": "Nhà cung cấp",
+ "clearFilters": "Xóa bộ lọc",
"sourceCloudAgent": "Tác nhân đám mây",
"sourceA2A": "A2A",
"sourceConductor": "Người dẫn dắt",
"sourceStale": "Không cập nhật từ {since}",
"sourceOffline": "Ngoại tuyến",
+ "sourceCollapse": "Thu gọn",
+ "sourceExpand": "Mở rộng",
"stateQueued": "Đang xếp hàng",
"stateRunning": "Đang chạy",
"stateWaitingApproval": "Chờ phê duyệt",
@@ -13964,11 +13971,14 @@
"drawerMetrics": "Chỉ số",
"drawerResult": "Kết quả",
"drawerActions": "Hành động",
+ "drawerClose": "Đóng",
+ "copyTrace": "Sao chép JSON theo dõi",
"actionApprove": "Phê duyệt kế hoạch",
"actionCancel": "Hủy",
"actionSeeInGraph": "Xem trong sơ đồ",
"actionDone": "Đã áp dụng hành động",
"actionFailed": "Hành động thất bại: {error}",
+ "detailFailed": "Không tải được chi tiết: {error}",
"mirroredInA2A": "Được phản chiếu trong A2A"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index 0d423362755..1cecd5e1e82 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -13935,11 +13935,18 @@
"tabRouting": "路由",
"tabOverview": "概览",
"showCompleted": "显示已完成",
+ "searchPlaceholder": "搜索任务…",
+ "filterStates": "状态",
+ "filterSources": "来源",
+ "filterProviders": "提供者",
+ "clearFilters": "清除筛选",
"sourceCloudAgent": "云代理",
"sourceA2A": "A2A",
"sourceConductor": "导体",
"sourceStale": "自 {since} 起未更新",
"sourceOffline": "离线",
+ "sourceCollapse": "折叠",
+ "sourceExpand": "展开",
"stateQueued": "已排队",
"stateRunning": "运行中",
"stateWaitingApproval": "等待批准",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "指标",
"drawerResult": "结果",
"drawerActions": "操作",
+ "drawerClose": "关闭",
+ "copyTrace": "复制追踪 JSON",
"actionApprove": "批准计划",
"actionCancel": "取消",
"actionSeeInGraph": "在图中查看",
"actionDone": "操作已应用",
"actionFailed": "操作失败:{error}",
+ "detailFailed": "加载详情失败:{error}",
"mirroredInA2A": "已在 A2A 中镜像"
},
"cliproxyProviderExposure": {
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index 08bf741f137..738e15e6bfe 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -13935,11 +13935,18 @@
"tabRouting": "路由",
"tabOverview": "總覽",
"showCompleted": "顯示已完成",
+ "searchPlaceholder": "搜尋任務…",
+ "filterStates": "狀態",
+ "filterSources": "來源",
+ "filterProviders": "提供者",
+ "clearFilters": "清除篩選",
"sourceCloudAgent": "雲端代理",
"sourceA2A": "A2A",
"sourceConductor": "導體",
"sourceStale": "自 {since} 起未更新",
"sourceOffline": "離線",
+ "sourceCollapse": "收合",
+ "sourceExpand": "展開",
"stateQueued": "佇列中",
"stateRunning": "執行中",
"stateWaitingApproval": "等待核准",
@@ -13956,11 +13963,14 @@
"drawerMetrics": "指標",
"drawerResult": "結果",
"drawerActions": "動作",
+ "drawerClose": "關閉",
+ "copyTrace": "複製追蹤 JSON",
"actionApprove": "核准計畫",
"actionCancel": "取消",
"actionSeeInGraph": "在圖中檢視",
"actionDone": "動作已套用",
"actionFailed": "動作失敗:{error}",
+ "detailFailed": "載入詳細資料失敗:{error}",
"mirroredInA2A": "已在 A2A 中鏡射"
},
"cliproxyProviderExposure": {
diff --git a/tests/unit/ui/orchestrationDrawer.test.tsx b/tests/unit/ui/orchestrationDrawer.test.tsx
index 5082b98fd0d..9142d22f1e2 100644
--- a/tests/unit/ui/orchestrationDrawer.test.tsx
+++ b/tests/unit/ui/orchestrationDrawer.test.tsx
@@ -8,7 +8,10 @@ vi.mock("next-intl", () => ({
v ? `${k}:${JSON.stringify(v)}` : k,
}));
-import { OrchestrationDrawer } from "@/app/(dashboard)/dashboard/orchestration/drawer/OrchestrationDrawer";
+import {
+ OrchestrationDrawer,
+ buildTraceJson,
+} from "@/app/(dashboard)/dashboard/orchestration/drawer/OrchestrationDrawer";
function render(el: React.ReactElement) {
const c = document.createElement("div");
@@ -240,4 +243,205 @@ describe("OrchestrationDrawer", () => {
expect(link).toBeTruthy();
cleanup();
});
+
+ it("close button aria-label comes from i18n (drawerClose, not the literal 'close')", () => {
+ const node = { id: "overflow:1", kind: "overflow", state: "running", label: "x", raw: {} };
+ const { c, cleanup } = render(
+
{}} onActionDone={() => {}} />
+ );
+ expect(c.querySelector('[aria-label="drawerClose"]')).toBeTruthy();
+ expect(c.querySelector('[aria-label="close"]')).toBeNull();
+ cleanup();
+ });
+
+ it("copy trace button copies buildTraceJson output to the clipboard and shows the actionDone toast", async () => {
+ const writeText = vi.fn(() => Promise.resolve());
+ Object.defineProperty(navigator, "clipboard", {
+ value: { writeText },
+ configurable: true,
+ });
+ const node = {
+ id: "overflow:1",
+ kind: "overflow",
+ state: "running",
+ label: "x",
+ raw: { a: 1 },
+ };
+ const { c, cleanup } = render(
+ {}} onActionDone={() => {}} />
+ );
+ const btn = c.querySelector('[aria-label="copyTrace"]') as HTMLButtonElement;
+ expect(btn).toBeTruthy();
+ await act(async () => {
+ btn.click();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(writeText).toHaveBeenCalledTimes(1);
+ expect(writeText.mock.calls[0][0]).toBe(buildTraceJson(node as never, node.raw));
+ expect(c.textContent).toContain("actionDone");
+ cleanup();
+ });
+
+ it("copy trace shows actionFailed:clipboard toast when the clipboard write rejects", async () => {
+ const writeText = vi.fn(() => Promise.reject(new Error("denied")));
+ Object.defineProperty(navigator, "clipboard", {
+ value: { writeText },
+ configurable: true,
+ });
+ const node = { id: "overflow:1", kind: "overflow", state: "running", label: "x", raw: {} };
+ const { c, cleanup } = render(
+ {}} onActionDone={() => {}} />
+ );
+ const btn = c.querySelector('[aria-label="copyTrace"]') as HTMLButtonElement;
+ await act(async () => {
+ btn.click();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(c.textContent).toContain("actionFailed");
+ expect(c.textContent).toContain("clipboard");
+ cleanup();
+ });
+
+ it("shows detailFailed for a fetch error and actionFailed for a subsequent action error", async () => {
+ const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
+ if (init?.method === "POST") {
+ return Promise.resolve({ ok: false, status: 500, json: () => Promise.resolve({}) });
+ }
+ return Promise.reject(new Error("network down"));
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ const node = {
+ id: "cloud-agent:t1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "waiting_approval",
+ label: "x",
+ };
+ const { c, cleanup } = render(
+ {}} onActionDone={() => {}} />
+ );
+ await act(async () => {
+ await Promise.resolve();
+ });
+ expect(c.textContent).toContain("detailFailed");
+ expect(c.textContent).not.toContain("actionFailed");
+
+ const btn = Array.from(c.querySelectorAll("button")).find((b) =>
+ b.textContent?.includes("actionApprove")
+ ) as HTMLButtonElement;
+ await act(async () => {
+ btn.click();
+ await Promise.resolve();
+ });
+ expect(c.textContent).toContain("actionFailed");
+ cleanup();
+ });
+
+ it("disables approve/cancel while an action promise is pending, and re-enables once it settles", async () => {
+ let resolvePost: ((v: unknown) => void) | undefined;
+ const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
+ if (init?.method === "POST") {
+ return new Promise((resolve) => {
+ resolvePost = resolve;
+ });
+ }
+ return Promise.resolve({
+ ok: true,
+ json: () =>
+ Promise.resolve({
+ data: {
+ id: "t1",
+ status: "awaiting_approval",
+ activities: [],
+ prompt: "",
+ providerId: "devin",
+ source: { repoName: "r", repoUrl: "https://x" },
+ options: {},
+ createdAt: "x",
+ updatedAt: "y",
+ },
+ }),
+ });
+ });
+ vi.stubGlobal("fetch", fetchMock);
+ const node = {
+ id: "cloud-agent:t1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "waiting_approval",
+ label: "x",
+ };
+ const { c, cleanup } = render(
+ {}} onActionDone={() => {}} />
+ );
+ await act(async () => {
+ await Promise.resolve();
+ });
+ const approveBtn = Array.from(c.querySelectorAll("button")).find((b) =>
+ b.textContent?.includes("actionApprove")
+ ) as HTMLButtonElement;
+ const cancelBtn = Array.from(c.querySelectorAll("button")).find((b) =>
+ b.textContent?.includes("actionCancel")
+ ) as HTMLButtonElement;
+ expect(approveBtn.disabled).toBe(false);
+ expect(cancelBtn.disabled).toBe(false);
+
+ await act(async () => {
+ approveBtn.click();
+ await Promise.resolve();
+ });
+ expect(approveBtn.disabled).toBe(true);
+ expect(cancelBtn.disabled).toBe(true);
+
+ await act(async () => {
+ resolvePost?.({ ok: true, json: () => Promise.resolve({ data: {} }) });
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(approveBtn.disabled).toBe(false);
+ expect(cancelBtn.disabled).toBe(false);
+ cleanup();
+ });
+});
+
+describe("buildTraceJson", () => {
+ it("normalizes cloud-agent timeline from detail.activities and includes node identity + raw", () => {
+ const node = {
+ id: "cloud-agent:t1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "x",
+ };
+ const detail = { activities: [{ id: "a1", type: "plan", content: "c" }] };
+ const parsed = JSON.parse(buildTraceJson(node as never, detail));
+ expect(parsed).toEqual({
+ node: { id: "cloud-agent:t1", source: "cloud-agent", state: "running", label: "x" },
+ timeline: detail.activities,
+ raw: detail,
+ });
+ });
+
+ it("normalizes a2a timeline from detail.events", () => {
+ const node = { id: "a2a:1", kind: "work", source: "a2a", state: "running", label: "x" };
+ const detail = { events: [{ state: "working", timestamp: "t" }] };
+ const parsed = JSON.parse(buildTraceJson(node as never, detail));
+ expect(parsed.timeline).toEqual(detail.events);
+ });
+
+ it("uses a null timeline and falls back to node.raw for conductor/overflow sources", () => {
+ const node = {
+ id: "conductor:task:1",
+ kind: "work",
+ source: "conductor",
+ state: "running",
+ label: "x",
+ raw: { foo: "bar" },
+ };
+ const parsed = JSON.parse(buildTraceJson(node as never, null));
+ expect(parsed.timeline).toBeNull();
+ expect(parsed.raw).toEqual({ foo: "bar" });
+ });
});
diff --git a/tests/unit/ui/orchestrationFilter.test.ts b/tests/unit/ui/orchestrationFilter.test.ts
new file mode 100644
index 00000000000..0936abb2c37
--- /dev/null
+++ b/tests/unit/ui/orchestrationFilter.test.ts
@@ -0,0 +1,244 @@
+/**
+ * tests/unit/ui/orchestrationFilter.test.ts
+ * Run: node --import tsx/esm --test tests/unit/ui/orchestrationFilter.test.ts
+ */
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+ EMPTY_FILTER,
+ filterSnapshot,
+ isEmptyFilter,
+ nodeProviderKey,
+ collectProviderKeys,
+ type OrchFilter,
+} from "../../../src/app/(dashboard)/dashboard/orchestration/model/filterSnapshot.ts";
+import type {
+ OrchNode,
+ OrchSnapshot,
+} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
+
+function filterWith(over: Partial): OrchFilter {
+ return { ...EMPTY_FILTER, ...over };
+}
+
+// Fixture: orchestrator + 3 sources + works:
+// cloud-agent:1 (label "fix login bug", sublabel "jules", raw {providerId:"jules"}, running)
+// + cloud-agent:1:activity (activity follows its parent work node)
+// a2a:2 (label "smart-routing", state succeeded, raw {})
+// conductor:task:3 (label "deploy", raw {runner:"vm-9"}, state failed)
+// overflow:cloud-agent (always survives; counts/droppedByState must stay untouched)
+const caNode: OrchNode = {
+ id: "cloud-agent:1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "fix login bug",
+ sublabel: "jules",
+ raw: { providerId: "jules" },
+};
+const caActivity: OrchNode = {
+ id: "cloud-agent:1:activity",
+ kind: "activity",
+ source: "cloud-agent",
+ label: "npm test",
+};
+const a2aNode: OrchNode = {
+ id: "a2a:2",
+ kind: "work",
+ source: "a2a",
+ state: "succeeded",
+ label: "smart-routing",
+ raw: {},
+};
+const condNode: OrchNode = {
+ id: "conductor:task:3",
+ kind: "work",
+ source: "conductor",
+ state: "failed",
+ label: "deploy",
+ raw: { runner: "vm-9" },
+};
+const overflowCounts = { failed: 3 };
+const overflowNode: OrchNode = {
+ id: "overflow:cloud-agent",
+ kind: "overflow",
+ source: "cloud-agent",
+ label: "+3 more",
+ counts: overflowCounts,
+ droppedByState: overflowCounts,
+};
+const sourceCloudAgent: OrchNode = {
+ id: "source:cloud-agent",
+ kind: "source",
+ source: "cloud-agent",
+ label: "cloud-agent",
+ counts: { running: 1 },
+};
+const sourceA2A: OrchNode = {
+ id: "source:a2a",
+ kind: "source",
+ source: "a2a",
+ label: "a2a",
+ counts: { succeeded: 1 },
+};
+const sourceConductor: OrchNode = {
+ id: "source:conductor",
+ kind: "source",
+ source: "conductor",
+ label: "conductor",
+ counts: { failed: 1 },
+};
+const orchestratorNode: OrchNode = { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" };
+
+const snap: OrchSnapshot = {
+ nodes: [
+ orchestratorNode,
+ sourceCloudAgent,
+ sourceA2A,
+ sourceConductor,
+ caNode,
+ caActivity,
+ a2aNode,
+ condNode,
+ overflowNode,
+ ],
+ edges: [
+ {
+ id: "e:orchestrator→source:cloud-agent",
+ from: "orchestrator",
+ to: "source:cloud-agent",
+ kind: "owns",
+ active: false,
+ },
+ {
+ id: "e:orchestrator→source:a2a",
+ from: "orchestrator",
+ to: "source:a2a",
+ kind: "owns",
+ active: false,
+ },
+ {
+ id: "e:orchestrator→source:conductor",
+ from: "orchestrator",
+ to: "source:conductor",
+ kind: "owns",
+ active: false,
+ },
+ {
+ id: "e:source:cloud-agent→cloud-agent:1",
+ from: "source:cloud-agent",
+ to: "cloud-agent:1",
+ kind: "owns",
+ active: true,
+ },
+ {
+ id: "e:cloud-agent:1→cloud-agent:1:activity",
+ from: "cloud-agent:1",
+ to: "cloud-agent:1:activity",
+ kind: "owns",
+ active: true,
+ },
+ {
+ id: "e:source:a2a→a2a:2",
+ from: "source:a2a",
+ to: "a2a:2",
+ kind: "owns",
+ active: false,
+ },
+ {
+ id: "e:source:conductor→conductor:task:3",
+ from: "source:conductor",
+ to: "conductor:task:3",
+ kind: "owns",
+ active: false,
+ },
+ {
+ id: "e:source:cloud-agent→overflow:cloud-agent",
+ from: "source:cloud-agent",
+ to: "overflow:cloud-agent",
+ kind: "owns",
+ active: false,
+ },
+ ],
+ sources: [
+ { source: "cloud-agent", ok: true },
+ { source: "a2a", ok: true },
+ { source: "conductor", ok: true },
+ ],
+ generatedAt: "2026-09-01T00:00:00Z",
+};
+
+test("empty filter returns the same reference", () => {
+ assert.equal(filterSnapshot(snap, EMPTY_FILTER), snap);
+ assert.equal(isEmptyFilter(EMPTY_FILTER), true);
+});
+
+test("q matches label case-insensitively and drops non-matching works + their activities", () => {
+ const out = filterSnapshot(snap, filterWith({ q: "SMART" }));
+ const ids = out.nodes.map((n) => n.id);
+ assert.ok(ids.includes("a2a:2"));
+ assert.ok(!ids.includes("cloud-agent:1"), "non-matching work dropped");
+ assert.ok(!ids.includes("cloud-agent:1:activity"), "its activity is dropped along with it");
+ assert.ok(!ids.includes("conductor:task:3"), "non-matching work dropped");
+});
+
+test("q matching a work keeps its activity node and the edges between them", () => {
+ const out = filterSnapshot(snap, filterWith({ q: "login" }));
+ const ids = out.nodes.map((n) => n.id);
+ assert.ok(ids.includes("cloud-agent:1"));
+ assert.ok(ids.includes("cloud-agent:1:activity"));
+ assert.ok(
+ out.edges.some((e) => e.from === "cloud-agent:1" && e.to === "cloud-agent:1:activity")
+ );
+});
+
+test("state chip keeps only matching works", () => {
+ const out = filterSnapshot(snap, filterWith({ states: new Set(["failed"]) }));
+ const works = out.nodes.filter((n) => n.kind === "work").map((n) => n.id);
+ assert.deepEqual(works, ["conductor:task:3"]);
+ assert.ok(!out.nodes.some((n) => n.id === "cloud-agent:1:activity"));
+});
+
+test("source chip keeps only matching works", () => {
+ const out = filterSnapshot(snap, filterWith({ sources: new Set(["a2a"]) }));
+ const works = out.nodes.filter((n) => n.kind === "work").map((n) => n.id);
+ assert.deepEqual(works, ["a2a:2"]);
+});
+
+test("provider chip matches nodeProviderKey", () => {
+ assert.equal(nodeProviderKey(caNode), "jules");
+ assert.equal(nodeProviderKey(a2aNode), null);
+ assert.equal(nodeProviderKey(condNode), "vm-9");
+
+ const out = filterSnapshot(snap, filterWith({ providers: new Set(["jules"]) }));
+ const works = out.nodes.filter((n) => n.kind === "work").map((n) => n.id);
+ assert.deepEqual(works, ["cloud-agent:1"]);
+});
+
+test("dimensions AND together", () => {
+ // "deploy" matches conductor:task:3's label, but that node is state "failed" — asking
+ // for state "running" too must yield no work survivors even though q alone would match.
+ const out = filterSnapshot(snap, filterWith({ q: "deploy", states: new Set(["running"]) }));
+ assert.equal(out.nodes.filter((n) => n.kind === "work").length, 0);
+});
+
+test("source/orchestrator/overflow nodes always survive; counts untouched", () => {
+ // A query that matches nothing at all — every work node is dropped.
+ const out = filterSnapshot(snap, filterWith({ q: "nothing-matches-this" }));
+ assert.equal(out.nodes.filter((n) => n.kind === "work").length, 0);
+
+ assert.ok(out.nodes.some((n) => n.id === "orchestrator"));
+ const sources = out.nodes.filter((n) => n.kind === "source");
+ assert.equal(sources.length, 3);
+ const survivedSourceCloudAgent = out.nodes.find((n) => n.id === "source:cloud-agent");
+ assert.equal(survivedSourceCloudAgent?.counts, sourceCloudAgent.counts);
+
+ const survivedOverflow = out.nodes.find((n) => n.id === "overflow:cloud-agent");
+ assert.ok(survivedOverflow, "overflow node must survive even with zero matching works");
+ assert.equal(survivedOverflow?.counts, overflowNode.counts);
+ assert.equal(survivedOverflow?.droppedByState, overflowNode.droppedByState);
+});
+
+test("collectProviderKeys returns sorted distinct non-null keys", () => {
+ assert.deepEqual(collectProviderKeys(snap), ["jules", "vm-9"]);
+});
diff --git a/tests/unit/ui/orchestrationModel.test.ts b/tests/unit/ui/orchestrationModel.test.ts
index 08a728e29a4..a653220852f 100644
--- a/tests/unit/ui/orchestrationModel.test.ts
+++ b/tests/unit/ui/orchestrationModel.test.ts
@@ -7,8 +7,8 @@ import assert from "node:assert/strict";
import {
ORCH_STATES,
orchStateColor,
+ orchStateBadgeBg,
} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
-import { STATUS_HEX } from "../../../src/shared/constants/statusColors.ts";
import { fromCloudAgent } from "../../../src/app/(dashboard)/dashboard/orchestration/model/fromCloudAgent.ts";
import type { CloudAgentTask } from "../../../src/lib/cloudAgent/types.ts";
import { fromA2A } from "../../../src/app/(dashboard)/dashboard/orchestration/model/fromA2A.ts";
@@ -22,20 +22,30 @@ import {
} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
describe("orchestrationTypes", () => {
- it("covers all six states with a color each", () => {
+ it("covers all six states with a theme-aware CSS var color each", () => {
assert.equal(ORCH_STATES.length, 6);
for (const s of ORCH_STATES) {
- assert.match(orchStateColor(s), /^#[0-9a-f]{6}$/i, s);
+ assert.match(orchStateColor(s), /^var\(--orch-status-[a-z]+\)$/, s);
}
});
- it("waiting_approval maps to the new STATUS_HEX.approval violet", () => {
- assert.equal(orchStateColor("waiting_approval"), STATUS_HEX.approval);
- assert.equal(STATUS_HEX.approval, "#8b5cf6");
+ it("waiting_approval maps to the approval token", () => {
+ assert.equal(orchStateColor("waiting_approval"), "var(--orch-status-approval)");
});
it("running maps to warning, succeeded to success, failed to error", () => {
- assert.equal(orchStateColor("running"), STATUS_HEX.warning);
- assert.equal(orchStateColor("succeeded"), STATUS_HEX.success);
- assert.equal(orchStateColor("failed"), STATUS_HEX.error);
+ assert.equal(orchStateColor("running"), "var(--orch-status-warning)");
+ assert.equal(orchStateColor("succeeded"), "var(--orch-status-success)");
+ assert.equal(orchStateColor("failed"), "var(--orch-status-error)");
+ });
+ it("queued and cancelled both map to the muted token", () => {
+ assert.equal(orchStateColor("queued"), "var(--orch-status-muted)");
+ assert.equal(orchStateColor("cancelled"), "var(--orch-status-muted)");
+ });
+ it("orchStateBadgeBg produces a color-mix over the matching state token", () => {
+ for (const s of ORCH_STATES) {
+ const bg = orchStateBadgeBg(s);
+ assert.match(bg, /^color-mix\(in srgb, var\(--orch-status-[a-z]+\) 13%, transparent\)$/, s);
+ assert.ok(bg.includes(orchStateColor(s)), `${s} badge bg should embed its own token`);
+ }
});
});
@@ -326,6 +336,45 @@ describe("mergeSnapshot", () => {
)
);
});
+ it("failed source placeholder carries the typed sourceIssue union alongside sublabel", () => {
+ const errSrc = [{ source: "conductor" as const, ok: false }];
+ const errSnap = mergeSnapshot({ cloudAgent: empty, a2a: empty, conductor: empty }, errSrc, {
+ now: NOW,
+ });
+ const errNode = errSnap.nodes.find((n) => n.id === "source:conductor");
+ assert.equal(errNode?.sourceIssue, "error");
+ assert.equal(errNode?.sublabel, "error");
+
+ const offlineSrc = [{ source: "conductor" as const, ok: true, offline: true }];
+ const offlineSnap = mergeSnapshot(
+ { cloudAgent: empty, a2a: empty, conductor: empty },
+ offlineSrc,
+ { now: NOW }
+ );
+ const offlineNode = offlineSnap.nodes.find((n) => n.id === "source:conductor");
+ assert.equal(offlineNode?.sourceIssue, "offline");
+ assert.equal(offlineNode?.sublabel, "offline");
+ });
+ it("failed source placeholder carries staleSince from the SourceStatus (undefined when the status has none)", () => {
+ const staleSince = "2026-09-01T12:00:00.000Z";
+ const errSrc = [{ source: "conductor" as const, ok: false, staleSince }];
+ const errSnap = mergeSnapshot({ cloudAgent: empty, a2a: empty, conductor: empty }, errSrc, {
+ now: NOW,
+ });
+ const errNode = errSnap.nodes.find((n) => n.id === "source:conductor");
+ assert.equal(errNode?.staleSince, staleSince);
+
+ // buildSourceStatuses never sets staleSince for the offline case — mergeSnapshot must
+ // not invent one, so the placeholder node's staleSince stays undefined.
+ const offlineSrc = [{ source: "conductor" as const, ok: true, offline: true }];
+ const offlineSnap = mergeSnapshot(
+ { cloudAgent: empty, a2a: empty, conductor: empty },
+ offlineSrc,
+ { now: NOW }
+ );
+ const offlineNode = offlineSnap.nodes.find((n) => n.id === "source:conductor");
+ assert.equal(offlineNode?.staleSince, undefined);
+ });
it("overflow node carries droppedByState with the per-state counts of dropped work nodes", () => {
const many = Array.from({ length: MAX_WORK_NODES + 5 }, (_, i) =>
caTask({
@@ -343,6 +392,25 @@ describe("mergeSnapshot", () => {
assert.ok(overflow, "overflow node expected");
assert.deepEqual(overflow?.droppedByState, { failed: 5 });
});
+ it("droppedByState is not a shared reference with counts — mutating counts after merge leaves it untouched", () => {
+ const many = Array.from({ length: MAX_WORK_NODES + 5 }, (_, i) =>
+ caTask({
+ id: `dbs${i}`,
+ status: i < MAX_WORK_NODES ? "running" : "failed",
+ updatedAt: new Date(NOW - i * 1000).toISOString(),
+ })
+ );
+ const snap = mergeSnapshot(
+ { cloudAgent: fromCloudAgent(many), a2a: empty, conductor: empty },
+ OK_SOURCES,
+ { now: NOW }
+ );
+ const overflow = snap.nodes.find((n) => n.id === "overflow:cloud-agent");
+ assert.ok(overflow, "overflow node expected");
+ const before = { ...overflow?.droppedByState };
+ if (overflow?.counts) overflow.counts.failed = 999;
+ assert.deepEqual(overflow?.droppedByState, before, "droppedByState must not alias counts");
+ });
it("drops a stale terminal Conductor task older than STALE_COMPLETED_MS unless showCompleted", () => {
const staleSnap: FleetSnapshot = {
...baseSnap,
diff --git a/tests/unit/ui/orchestrationNodes.test.tsx b/tests/unit/ui/orchestrationNodes.test.tsx
index c69ec4971d9..f98429700e3 100644
--- a/tests/unit/ui/orchestrationNodes.test.tsx
+++ b/tests/unit/ui/orchestrationNodes.test.tsx
@@ -19,8 +19,13 @@ vi.mock("next-intl", () => ({
v ? `${k}:${JSON.stringify(v)}` : k,
}));
+import type { EdgeProps } from "@xyflow/react";
import { WorkNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/WorkNode";
import { SourceNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/SourceNode";
+import { OrchestratorNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/OrchestratorNode";
+import { ActivityNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/ActivityNode";
+import { OverflowNode } from "@/app/(dashboard)/dashboard/orchestration/nodes/OverflowNode";
+import { StatusEdge } from "@/app/(dashboard)/dashboard/orchestration/edges/StatusEdge";
function render(el: React.ReactElement) {
const c = document.createElement("div");
@@ -55,16 +60,190 @@ describe("orchestration nodes", () => {
expect(c.querySelector("[aria-label]")).toBeTruthy();
cleanup();
});
- it("SourceNode with sublabel=error shows the warning marker", () => {
+ it("SourceNode with sourceIssue=error shows the warning marker", () => {
const data = {
id: "source:a2a",
kind: "source",
source: "a2a",
label: "A2A",
sublabel: "error",
+ sourceIssue: "error",
};
const { c, cleanup } = render();
expect(c.textContent).toContain("⚠");
cleanup();
});
+
+ it("SourceNode with sourceIssue=error and a parseable staleSince renders the formatted time in sourceStale", () => {
+ const staleSince = "2026-09-01T12:34:56.000Z";
+ const data = {
+ id: "source:a2a",
+ kind: "source",
+ source: "a2a",
+ label: "A2A",
+ sublabel: "error",
+ sourceIssue: "error",
+ staleSince,
+ };
+ const { c, cleanup } = render();
+ // Mocked next-intl `t` returns `${key}:${JSON.stringify(values)}` when values are passed —
+ // asserts the component actually forwards `{ since }`, not just the raw key.
+ const since = new Date(staleSince).toLocaleTimeString();
+ expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since })}`);
+ cleanup();
+ });
+
+ it("SourceNode with sourceIssue=error and no staleSince falls back to an em dash", () => {
+ const data = {
+ id: "source:a2a",
+ kind: "source",
+ source: "a2a",
+ label: "A2A",
+ sublabel: "error",
+ sourceIssue: "error",
+ };
+ const { c, cleanup } = render();
+ expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since: "—" })}`);
+ cleanup();
+ });
+
+ it("SourceNode with sourceIssue=error and an unparseable staleSince also falls back to an em dash", () => {
+ const data = {
+ id: "source:a2a",
+ kind: "source",
+ source: "a2a",
+ label: "A2A",
+ sublabel: "error",
+ sourceIssue: "error",
+ staleSince: "not-a-date",
+ };
+ const { c, cleanup } = render();
+ expect(c.textContent).toContain(`sourceStale:${JSON.stringify({ since: "—" })}`);
+ cleanup();
+ });
+
+ it("all 5 memo'd orchestration node components have a displayName", () => {
+ expect(WorkNode.displayName).toBe("WorkNode");
+ expect(SourceNode.displayName).toBe("SourceNode");
+ expect(OrchestratorNode.displayName).toBe("OrchestratorNode");
+ expect(ActivityNode.displayName).toBe("ActivityNode");
+ expect(OverflowNode.displayName).toBe("OverflowNode");
+ });
+
+ it("OrchestratorNode renders the label as text content and as aria-label", () => {
+ const data = { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" };
+ const { c, cleanup } = render();
+ expect(c.textContent).toContain("OmniRoute");
+ expect(c.querySelector('[aria-label="OmniRoute"]')).toBeTruthy();
+ cleanup();
+ });
+
+ it("ActivityNode renders label + sublabel and exposes the label as aria-label", () => {
+ const data = {
+ id: "cloud-agent:t1:activity",
+ kind: "activity",
+ source: "cloud-agent",
+ label: "npm test",
+ sublabel: "command",
+ };
+ const { c, cleanup } = render();
+ expect(c.textContent).toContain("npm test");
+ expect(c.textContent).toContain("command");
+ expect(c.querySelector('[aria-label="npm test"]')).toBeTruthy();
+ cleanup();
+ });
+
+ it("OverflowNode renders the overflowMore count in text + aria-label and a badge per non-zero state", () => {
+ const data = {
+ id: "overflow:cloud-agent",
+ kind: "overflow",
+ source: "cloud-agent",
+ label: "+7 more",
+ counts: { running: 5, failed: 2, queued: 0 },
+ };
+ const { c, cleanup } = render();
+ // Mocked next-intl `t` returns `${key}:${JSON.stringify(values)}` — asserts the
+ // component forwards the summed total (5 + 2 = 7), not a raw/stale count.
+ const expectedText = `overflowMore:${JSON.stringify({ count: 7 })}`;
+ expect(c.textContent).toContain(expectedText);
+ const labelled = c.querySelector("[aria-label]");
+ expect(labelled?.getAttribute("aria-label")).toBe(expectedText);
+ // One badge span per state with a non-zero count (running, failed) — queued (0) omitted.
+ const badges = Array.from(c.querySelectorAll("div.flex.gap-1 span"));
+ expect(badges.length).toBe(2);
+ expect(badges.map((b) => b.textContent)).toEqual(["5", "2"]);
+ cleanup();
+ });
+
+ it("OverflowNode with no counts renders a zero total and no per-state badges", () => {
+ const data = { id: "overflow:a2a", kind: "overflow", source: "a2a", label: "+0 more" };
+ const { c, cleanup } = render();
+ expect(c.textContent).toContain(`overflowMore:${JSON.stringify({ count: 0 })}`);
+ expect(c.querySelectorAll("div.flex.gap-1 span").length).toBe(0);
+ cleanup();
+ });
+
+ it("SourceNode shows a collapse caret + aria-expanded + title reflecting !data.collapsed", () => {
+ const expandedData = { id: "source:a2a", kind: "source", source: "a2a", label: "A2A" };
+ const r1 = render();
+ const expandedEl = r1.c.querySelector("[aria-expanded]");
+ expect(expandedEl?.getAttribute("aria-expanded")).toBe("true");
+ expect(expandedEl?.getAttribute("title")).toBe("sourceCollapse");
+ expect(r1.c.textContent).toContain("▾");
+ r1.cleanup();
+
+ const collapsedData = { ...expandedData, collapsed: true };
+ const r2 = render();
+ const collapsedEl = r2.c.querySelector("[aria-expanded]");
+ expect(collapsedEl?.getAttribute("aria-expanded")).toBe("false");
+ expect(collapsedEl?.getAttribute("title")).toBe("sourceExpand");
+ expect(r2.c.textContent).toContain("▸");
+ r2.cleanup();
+ });
+});
+
+describe("StatusEdge", () => {
+ const baseProps = {
+ id: "e1",
+ source: "a",
+ target: "b",
+ sourceX: 0,
+ sourceY: 0,
+ targetX: 100,
+ targetY: 100,
+ sourcePosition: "bottom",
+ targetPosition: "top",
+ };
+
+ it("data.active renders PARTICLES ellipses, each starting opacity=0 with a paired reveal", () => {
+ const props = { ...baseProps, data: { state: "running", active: true, mirror: false } };
+ const { c, cleanup } = render(
+
+ );
+ const ellipses = c.querySelectorAll("ellipse.orch-edge-particle");
+ expect(ellipses.length).toBe(3);
+ ellipses.forEach((ellipse) => {
+ expect(ellipse.getAttribute("opacity")).toBe("0");
+ const set = ellipse.querySelector("set");
+ expect(set).toBeTruthy();
+ expect(set?.getAttribute("attributeName")).toBe("opacity");
+ expect(set?.getAttribute("to")).toBe("1");
+ expect(set?.getAttribute("fill")).toBe("freeze");
+ expect(ellipse.querySelector("animateMotion")).toBeTruthy();
+ });
+ cleanup();
+ });
+
+ it("without data.active there is no particle ellipse", () => {
+ const props = { ...baseProps, data: { state: "succeeded", active: false, mirror: false } };
+ const { c, cleanup } = render(
+
+ );
+ expect(c.querySelectorAll("ellipse.orch-edge-particle").length).toBe(0);
+ cleanup();
+ });
});
diff --git a/tests/unit/ui/orchestrationPage.test.tsx b/tests/unit/ui/orchestrationPage.test.tsx
index 95f69535419..299dd2efada 100644
--- a/tests/unit/ui/orchestrationPage.test.tsx
+++ b/tests/unit/ui/orchestrationPage.test.tsx
@@ -9,18 +9,20 @@ vi.mock("next-intl", () => ({
}));
const replaceMock = vi.fn();
+const searchState = { current: "tab=overview" };
vi.mock("next/navigation", () => ({
- useSearchParams: () => new URLSearchParams("tab=overview"),
+ useSearchParams: () => new URLSearchParams(searchState.current),
useRouter: () => ({ replace: replaceMock }),
usePathname: () => "/dashboard/orchestration",
}));
-const snapshot = {
+const DEFAULT_SNAPSHOT = {
nodes: [{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" }],
edges: [],
sources: [],
generatedAt: "x",
};
+let snapshot: typeof DEFAULT_SNAPSHOT = DEFAULT_SNAPSHOT;
const setShowCompletedMock = vi.fn();
const refetchMock = vi.fn();
vi.mock("@/app/(dashboard)/dashboard/orchestration/hooks/useOrchestrationSnapshot", () => ({
@@ -45,14 +47,30 @@ vi.mock("@/hooks/useProviderBreakerHealth", () => ({
useProviderBreakerHealth: () => ({ providerHealth: {}, connectionHealth: {} }),
}));
+const agentsTabCalls: Record[] = [];
vi.mock("@/app/(dashboard)/dashboard/orchestration/tabs/AgentsTab", () => ({
- AgentsTab: () => ,
+ AgentsTab: (props: Record) => {
+ agentsTabCalls.push(props);
+ return ;
+ },
}));
vi.mock("@/app/(dashboard)/dashboard/orchestration/tabs/RoutingTab", () => ({
RoutingTab: () => ,
}));
+const overviewTabCalls: Record[] = [];
vi.mock("@/app/(dashboard)/dashboard/orchestration/tabs/OverviewTab", () => ({
- OverviewTab: () => ,
+ OverviewTab: (props: Record) => {
+ overviewTabCalls.push(props);
+ return ;
+ },
+}));
+
+const drawerCalls: Record[] = [];
+vi.mock("@/app/(dashboard)/dashboard/orchestration/drawer/OrchestrationDrawer", () => ({
+ OrchestrationDrawer: (props: Record) => {
+ drawerCalls.push(props);
+ return ;
+ },
}));
import OrchestrationPageClient from "@/app/(dashboard)/dashboard/orchestration/OrchestrationPageClient";
@@ -73,6 +91,11 @@ function render(el: React.ReactElement) {
afterEach(() => {
document.body.innerHTML = "";
replaceMock.mockClear();
+ searchState.current = "tab=overview";
+ snapshot = DEFAULT_SNAPSHOT;
+ agentsTabCalls.length = 0;
+ overviewTabCalls.length = 0;
+ drawerCalls.length = 0;
});
describe("OrchestrationPageClient", () => {
@@ -99,4 +122,135 @@ describe("OrchestrationPageClient", () => {
expect(opts).toEqual({ scroll: false });
cleanup();
});
+
+ it("?q=login filters the snapshot passed to OverviewTab down to matching work nodes", () => {
+ snapshot = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ {
+ id: "cloud-agent:1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "login flow fix",
+ },
+ {
+ id: "a2a:2",
+ kind: "work",
+ source: "a2a",
+ state: "failed",
+ label: "unrelated task",
+ },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ } as never;
+ searchState.current = "tab=overview&q=login";
+ const { cleanup } = render();
+ const lastProps = overviewTabCalls.at(-1) as { snapshot: typeof DEFAULT_SNAPSHOT };
+ const ids = lastProps.snapshot.nodes.map((n) => n.id);
+ expect(ids).toContain("cloud-agent:1");
+ expect(ids).not.toContain("a2a:2");
+ cleanup();
+ });
+
+ it("clicking a state chip in the toolbar sets ?state= via router.replace", () => {
+ searchState.current = "tab=agents";
+ const { c, cleanup } = render();
+ const runningChip = Array.from(c.querySelectorAll("button")).find(
+ (el) => el.textContent === "stateRunning"
+ ) as HTMLButtonElement;
+ expect(runningChip).toBeTruthy();
+ act(() => {
+ runningChip.click();
+ });
+ expect(replaceMock).toHaveBeenCalledTimes(1);
+ const [url] = replaceMock.mock.calls[0];
+ expect(url).toContain("state=running");
+ cleanup();
+ });
+
+ it("toggling a collapse from AgentsTab writes ?collapsed= via router.replace", () => {
+ searchState.current = "tab=agents";
+ const { cleanup } = render();
+ const props = agentsTabCalls.at(-1) as { onToggleCollapse: (s: string) => void };
+ act(() => {
+ props.onToggleCollapse("a2a");
+ });
+ expect(replaceMock).toHaveBeenCalledTimes(1);
+ const [url] = replaceMock.mock.calls[0];
+ expect(url).toContain("collapsed=a2a");
+ cleanup();
+ });
+
+ it("shows a clear-filters button only when the filter is non-empty, and it resets q/state/source/provider", () => {
+ searchState.current = "tab=agents";
+ const r1 = render();
+ expect(
+ Array.from(r1.c.querySelectorAll("button")).find((el) => el.textContent === "clearFilters")
+ ).toBeFalsy();
+ r1.cleanup();
+
+ searchState.current =
+ "tab=agents&q=login&state=running&source=a2a&provider=devin&collapsed=a2a";
+ const r2 = render();
+ const clearButton = Array.from(r2.c.querySelectorAll("button")).find(
+ (el) => el.textContent === "clearFilters"
+ ) as HTMLButtonElement;
+ expect(clearButton).toBeTruthy();
+ act(() => {
+ clearButton.click();
+ });
+ expect(replaceMock).toHaveBeenCalledTimes(1);
+ const [url] = replaceMock.mock.calls[0];
+ expect(url).not.toContain("q=");
+ expect(url).not.toContain("state=");
+ expect(url).not.toContain("source=");
+ expect(url).not.toContain("provider=");
+ expect(url).toContain("collapsed=a2a");
+ r2.cleanup();
+ });
+
+ it("?node= opens the drawer with the matching node; removing the param closes it", () => {
+ snapshot = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ {
+ id: "cloud-agent:1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "task A",
+ },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ } as never;
+
+ searchState.current = "tab=agents&node=cloud-agent:1";
+ const r1 = render();
+ expect((drawerCalls.at(-1) as { node: { id: string } | null }).node?.id).toBe("cloud-agent:1");
+ r1.cleanup();
+
+ searchState.current = "tab=agents";
+ const r2 = render();
+ expect((drawerCalls.at(-1) as { node: { id: string } | null }).node).toBeNull();
+ r2.cleanup();
+ });
+
+ it("clicking an overflow node (via AgentsTab's onNodeClick) navigates to ?tab=overview and clears ?node", () => {
+ searchState.current = "tab=agents&node=cloud-agent:1";
+ const { cleanup } = render();
+ const props = agentsTabCalls.at(-1) as { onNodeClick: (id: string) => void };
+ act(() => {
+ props.onNodeClick("overflow:cloud-agent");
+ });
+ expect(replaceMock).toHaveBeenCalledTimes(1);
+ const [url] = replaceMock.mock.calls[0];
+ expect(url).toContain("tab=overview");
+ expect(url).not.toContain("node=");
+ cleanup();
+ });
});
diff --git a/tests/unit/ui/orchestrationTabs.test.tsx b/tests/unit/ui/orchestrationTabs.test.tsx
index ce17e6fb162..b44ec79402e 100644
--- a/tests/unit/ui/orchestrationTabs.test.tsx
+++ b/tests/unit/ui/orchestrationTabs.test.tsx
@@ -89,6 +89,129 @@ describe("AgentsTab", () => {
expect(r2.c.querySelector(".orchestration-canvas")).toBeTruthy();
r2.cleanup();
});
+
+ it("passes the collapsed set to orchestrationToFlow, dropping that source's work nodes", () => {
+ const snap = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ { id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
+ { id: "a2a:1", kind: "work", source: "a2a", state: "running", label: "a2a task" },
+ {
+ id: "cloud-agent:1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "ca task",
+ },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ };
+ const { cleanup } = render(
+ {}}
+ showCompleted={false}
+ onToggleCompleted={() => {}}
+ collapsed={new Set(["a2a"])}
+ onToggleCollapse={() => {}}
+ />
+ );
+ const nodes = flowProps.at(-1)?.nodes as Array<{ id: string }>;
+ expect(nodes.some((n) => n.id === "a2a:1")).toBe(false);
+ expect(nodes.some((n) => n.id === "cloud-agent:1")).toBe(true);
+ cleanup();
+ });
+
+ it("clicking a source node calls onToggleCollapse with its source, not onNodeClick", () => {
+ const toggle = vi.fn();
+ const onNodeClick = vi.fn();
+ const snap = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ { id: "a2a:1", kind: "work", source: "a2a", state: "running", label: "a2a task" },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ };
+ const { cleanup } = render(
+ {}}
+ collapsed={new Set()}
+ onToggleCollapse={toggle}
+ />
+ );
+ const handleClick = flowProps.at(-1)?.onNodeClick as (e: unknown, node: unknown) => void;
+ handleClick(undefined, { id: "source:a2a", type: "source", data: { source: "a2a" } });
+ expect(toggle).toHaveBeenCalledWith("a2a");
+ expect(onNodeClick).not.toHaveBeenCalled();
+ cleanup();
+ });
+
+ it("clicking the orchestrator node is a no-op — neither onNodeClick nor onToggleCollapse fires", () => {
+ const toggle = vi.fn();
+ const onNodeClick = vi.fn();
+ const snap = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ { id: "a2a:1", kind: "work", source: "a2a", state: "running", label: "a2a task" },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ };
+ const { cleanup } = render(
+ {}}
+ collapsed={new Set()}
+ onToggleCollapse={toggle}
+ />
+ );
+ const handleClick = flowProps.at(-1)?.onNodeClick as (e: unknown, node: unknown) => void;
+ handleClick(undefined, { id: "orchestrator", type: "orchestrator", data: {} });
+ expect(toggle).not.toHaveBeenCalled();
+ expect(onNodeClick).not.toHaveBeenCalled();
+ cleanup();
+ });
+
+ it("toggling the showCompleted checkbox calls onToggleCompleted with the new checked value", () => {
+ const onToggleCompleted = vi.fn();
+ const snap = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ { id: "a2a:1", kind: "work", source: "a2a", state: "running", label: "a2a task" },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "x",
+ };
+ const { c, cleanup } = render(
+ {}}
+ showCompleted={false}
+ onToggleCompleted={onToggleCompleted}
+ />
+ );
+ const checkbox = c.querySelector('input[type="checkbox"]') as HTMLInputElement;
+ expect(checkbox).toBeTruthy();
+ expect(checkbox.checked).toBe(false);
+ act(() => {
+ // React listens to the native "click" event for checkboxes to trigger the
+ // synthetic onChange (same pattern as engineConfigForm.test.tsx).
+ checkbox.click();
+ });
+ expect(onToggleCompleted).toHaveBeenCalledWith(true);
+ cleanup();
+ });
});
describe("OverviewTab", () => {
@@ -138,6 +261,35 @@ describe("OverviewTab", () => {
expect(clicked).toBe("cloud-agent:1");
cleanup();
});
+ it("formatElapsed guards against an unparseable startedAt and renders an em dash", () => {
+ const snapBadStart = {
+ ...snap,
+ nodes: [
+ ...snap.nodes,
+ {
+ id: "a2a:bad",
+ kind: "work",
+ source: "a2a",
+ state: "running",
+ label: "task bad start",
+ startedAt: "not-a-date",
+ },
+ ],
+ };
+ const { c, cleanup } = render(
+ {}}
+ onSeeInGraph={() => {}}
+ />
+ );
+ const card = Array.from(c.querySelectorAll("[data-orch-card]")).find((el) =>
+ el.textContent?.includes("task bad start")
+ );
+ expect(card?.textContent).toContain("—");
+ cleanup();
+ });
});
describe("RoutingTab", () => {
diff --git a/tests/unit/ui/orchestrationToFlow.test.ts b/tests/unit/ui/orchestrationToFlow.test.ts
index e3796adcb5f..e4879301e78 100644
--- a/tests/unit/ui/orchestrationToFlow.test.ts
+++ b/tests/unit/ui/orchestrationToFlow.test.ts
@@ -20,6 +20,38 @@ const snap: OrchSnapshot = {
generatedAt: "2026-08-30T12:00:00Z",
};
+const multiSourceSnap: OrchSnapshot = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ { id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
+ { id: "a2a:t1", kind: "work", source: "a2a", state: "running", label: "smart-routing" },
+ {
+ id: "a2a:t1:activity",
+ kind: "activity",
+ source: "a2a",
+ state: "running",
+ label: "thinking",
+ },
+ { id: "source:cloud-agent", kind: "source", source: "cloud-agent", label: "Cloud Agent" },
+ {
+ id: "cloud-agent:t1",
+ kind: "work",
+ source: "cloud-agent",
+ state: "queued",
+ label: "build",
+ },
+ ],
+ edges: [
+ { id: "e1", from: "orchestrator", to: "source:a2a", kind: "owns", active: false },
+ { id: "e2", from: "source:a2a", to: "a2a:t1", kind: "owns", active: true },
+ { id: "e3", from: "a2a:t1", to: "a2a:t1:activity", kind: "owns", active: true },
+ { id: "e4", from: "orchestrator", to: "source:cloud-agent", kind: "owns", active: false },
+ { id: "e5", from: "source:cloud-agent", to: "cloud-agent:t1", kind: "owns", active: false },
+ ],
+ sources: [],
+ generatedAt: "2026-08-30T12:00:00Z",
+};
+
describe("orchestrationToFlow", () => {
it("puts each kind on its own Y layer and is deterministic", () => {
const a = orchestrationToFlow(snap);
@@ -33,11 +65,32 @@ describe("orchestrationToFlow", () => {
assert.equal(ys.get("source:a2a"), 150);
assert.equal(ys.get("a2a:t1"), 320);
});
- it("active edge is animated; edge to failed work is red", () => {
+ it('edges carry type "status" and data.{state,active,mirror}; no animated/style leak', () => {
const { edges } = orchestrationToFlow(snap);
- assert.equal(edges.find((e) => e.id === "e2")?.animated, true);
- const failedEdge = edges.find((e) => e.id === "e3");
- assert.equal((failedEdge?.style as { stroke?: string })?.stroke, "#ef4444");
+ const activeEdge = edges.find((e) => e.id === "e2");
+ assert.equal(activeEdge?.type, "status");
+ assert.deepEqual(activeEdge?.data, { state: "running", active: true, mirror: false });
+ assert.equal((activeEdge as { animated?: boolean }).animated, undefined);
+ assert.equal((activeEdge as { style?: unknown }).style, undefined);
+
+ const edgeToFailed = edges.find((e) => e.id === "e3");
+ assert.equal(edgeToFailed?.type, "status");
+ assert.deepEqual(edgeToFailed?.data, { state: "failed", active: false, mirror: false });
+ });
+ it("mirror edges carry data.mirror === true", () => {
+ const mirrorSnap: OrchSnapshot = {
+ ...snap,
+ edges: [
+ ...snap.edges,
+ { id: "e4", from: "a2a:t1", to: "source:a2a", kind: "mirror", active: false },
+ ],
+ };
+ const { edges } = orchestrationToFlow(mirrorSnap);
+ const mirrorEdge = edges.find((e) => e.id === "e4");
+ assert.equal(mirrorEdge?.type, "status");
+ assert.equal((mirrorEdge?.data as { mirror?: boolean })?.mirror, true);
+ const ownsEdge = edges.find((e) => e.id === "e2");
+ assert.equal((ownsEdge?.data as { mirror?: boolean })?.mirror, false);
});
it("fitKey only tracks the set of work ids", () => {
const k1 = orchestrationToFlow(snap).fitKey;
@@ -53,4 +106,43 @@ describe("orchestrationToFlow", () => {
};
assert.notEqual(orchestrationToFlow(nodeRemoved).fitKey, k1);
});
+
+ it("opts omitted preserves current behavior (all nodes/edges kept, no collapsed data)", () => {
+ const { nodes, edges, fitKey } = orchestrationToFlow(multiSourceSnap);
+ assert.equal(nodes.length, multiSourceSnap.nodes.length);
+ assert.equal(edges.length, multiSourceSnap.edges.length);
+ assert.ok(!fitKey.includes("::collapsed="));
+ const sourceA2a = nodes.find((n) => n.id === "source:a2a");
+ assert.equal((sourceA2a?.data as { collapsed?: boolean }).collapsed, undefined);
+ });
+
+ it("collapsing a source removes its work/activity nodes and their edges, keeps other sources", () => {
+ const { nodes, edges } = orchestrationToFlow(multiSourceSnap, {
+ collapsed: new Set(["a2a"]),
+ });
+ const ids = nodes.map((n) => n.id).sort();
+ assert.deepEqual(ids, ["cloud-agent:t1", "orchestrator", "source:a2a", "source:cloud-agent"]);
+ const edgeIds = edges.map((e) => e.id).sort();
+ assert.deepEqual(edgeIds, ["e1", "e4", "e5"]);
+ });
+
+ it("SourceNode for a collapsed source carries data.collapsed === true; others do not", () => {
+ const { nodes } = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) });
+ const sourceA2a = nodes.find((n) => n.id === "source:a2a");
+ const sourceCloudAgent = nodes.find((n) => n.id === "source:cloud-agent");
+ assert.equal((sourceA2a?.data as { collapsed?: boolean }).collapsed, true);
+ assert.equal((sourceCloudAgent?.data as { collapsed?: boolean }).collapsed, undefined);
+ });
+
+ it("fitKey changes when the collapsed set changes and is stable otherwise", () => {
+ const base = orchestrationToFlow(multiSourceSnap).fitKey;
+ const k1 = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) }).fitKey;
+ const k1Again = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) }).fitKey;
+ const k2 = orchestrationToFlow(multiSourceSnap, {
+ collapsed: new Set(["cloud-agent"]),
+ }).fitKey;
+ assert.equal(k1, k1Again);
+ assert.notEqual(k1, base);
+ assert.notEqual(k1, k2);
+ });
});
diff --git a/tests/unit/ui/overviewProjection.test.ts b/tests/unit/ui/overviewProjection.test.ts
index 56c133cc96a..1a86a44abc7 100644
--- a/tests/unit/ui/overviewProjection.test.ts
+++ b/tests/unit/ui/overviewProjection.test.ts
@@ -44,6 +44,58 @@ describe("overviewProjection", () => {
assert.equal(columns.done[0].id, "a2a:3");
assert.equal(columns.running.length, 1);
});
+ it("sorts the done column by updatedAt descending, with 3+ terminal items interleaved out of order", () => {
+ // NOTE: the implementation (model/overviewProjection.ts) sorts
+ // `columns.done` by `updatedAt` descending, not `endedAt` — verified by reading the
+ // source before writing this assertion. Interleaved with a non-terminal (`running`)
+ // node to also confirm it never lands in `done`.
+ const interleaved: OrchSnapshot = {
+ nodes: [
+ { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
+ {
+ id: "a2a:oldest",
+ kind: "work",
+ source: "a2a",
+ state: "failed",
+ label: "oldest",
+ updatedAt: "2026-08-30T09:00:00Z",
+ },
+ {
+ id: "cloud-agent:running",
+ kind: "work",
+ source: "cloud-agent",
+ state: "running",
+ label: "not terminal",
+ updatedAt: "2026-08-30T13:00:00Z",
+ },
+ {
+ id: "a2a:newest",
+ kind: "work",
+ source: "a2a",
+ state: "succeeded",
+ label: "newest",
+ updatedAt: "2026-08-30T12:00:00Z",
+ },
+ {
+ id: "cloud-agent:middle",
+ kind: "work",
+ source: "cloud-agent",
+ state: "cancelled",
+ label: "middle",
+ updatedAt: "2026-08-30T10:30:00Z",
+ },
+ ],
+ edges: [],
+ sources: [],
+ generatedAt: "2026-08-30T13:00:00Z",
+ };
+ const { columns } = overviewProjection(interleaved, 0);
+ assert.deepEqual(
+ columns.done.map((n) => n.id),
+ ["a2a:newest", "cloud-agent:middle", "a2a:oldest"]
+ );
+ assert.equal(columns.running.length, 1);
+ });
it("folds an overflow node's droppedByState into counts but not into columns", () => {
const snapWithOverflow: OrchSnapshot = {
...snap,
diff --git a/tests/unit/ui/useOrchestrationSnapshot.test.tsx b/tests/unit/ui/useOrchestrationSnapshot.test.tsx
index d90e1d8a2a3..5160f8039dc 100644
--- a/tests/unit/ui/useOrchestrationSnapshot.test.tsx
+++ b/tests/unit/ui/useOrchestrationSnapshot.test.tsx
@@ -118,4 +118,61 @@ describe("useOrchestrationSnapshot", () => {
// Two burst events → exactly ONE extra round of 3 fetches (debounce), not two.
expect(fetchMock.mock.calls.length).toBe(callsAfterMount + 3);
});
+
+ it("keeps snapshot referential identity across polls with an unchanged payload, and mints a new one when it changes", async () => {
+ let latest: ReturnType | null = null;
+ const task = {
+ id: "t1",
+ providerId: "devin",
+ status: "running",
+ prompt: "p",
+ source: { repoName: "r", repoUrl: "https://x" },
+ options: {},
+ activities: [],
+ createdAt: "2026-08-30T10:00:00Z",
+ updatedAt: "2026-08-30T10:00:00Z",
+ };
+ const fetchMock = vi.fn((url: string) => {
+ if (url.startsWith("/api/v1/agents/tasks")) return okJson({ data: [task] });
+ if (url.startsWith("/api/a2a/tasks"))
+ return okJson({ tasks: [], total: 0, limit: 200, offset: 0 });
+ return okJson({ offline: false, runners: [], tasks: [] });
+ });
+ vi.stubGlobal("fetch", fetchMock);
+
+ await act(async () => {
+ root.render(
+ {
+ latest = v;
+ }}
+ />
+ );
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(10);
+ });
+ const firstSnapshot = latest!.snapshot;
+ expect(firstSnapshot.nodes.some((n) => n.id === "cloud-agent:t1")).toBe(true);
+
+ // Same payload next poll tick → `polledAt` advances but content doesn't,
+ // so the hook must keep returning the SAME snapshot object.
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(5_100);
+ });
+ expect(latest!.snapshot).toBe(firstSnapshot);
+
+ // Payload actually changes → a new snapshot identity is expected.
+ fetchMock.mockImplementation((url: string) => {
+ if (url.startsWith("/api/v1/agents/tasks"))
+ return okJson({ data: [{ ...task, status: "completed" }] });
+ if (url.startsWith("/api/a2a/tasks"))
+ return okJson({ tasks: [], total: 0, limit: 200, offset: 0 });
+ return okJson({ offline: false, runners: [], tasks: [] });
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(5_100);
+ });
+ expect(latest!.snapshot).not.toBe(firstSnapshot);
+ });
});