@polderlabs/bizar 6.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/sessionend-recall.mjs +3 -3
- package/.claude/hooks/worker-suggest.mjs +110 -0
- package/.claude/settings.json +9 -0
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
- package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
- package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
- package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
- package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
- package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
- package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
- package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
- package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
- package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
- package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
- package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
- package/bizar-dash/dist/index.html +12 -10
- package/bizar-dash/dist/mobile.html +7 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +11 -0
- package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
- package/bizar-dash/src/server/memory-distillation.mjs +296 -0
- package/bizar-dash/src/server/routes/distill.mjs +129 -0
- package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
- package/bizar-dash/src/web/App.tsx +4 -0
- package/bizar-dash/src/web/components/Tag.tsx +18 -5
- package/bizar-dash/src/web/components/Topbar.tsx +3 -0
- package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
- package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
- package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
- package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
- package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
- package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
- package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
- package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
- package/bizar-dash/src/web/lib/types.ts +25 -0
- package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
- package/bizar-dash/src/web/styles/main.css +251 -0
- package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
- package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
- package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
- package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
- package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
- package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
- package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
- package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
- package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
- package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
- package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
- package/bizar-dash/src/web/ui/controls/controls.css +454 -0
- package/bizar-dash/src/web/ui/controls/index.ts +33 -0
- package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
- package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
- package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
- package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
- package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
- package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
- package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
- package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
- package/bizar-dash/src/web/ui/data/data.css +535 -0
- package/bizar-dash/src/web/ui/data/index.ts +31 -0
- package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
- package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
- package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
- package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
- package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
- package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
- package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
- package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
- package/bizar-dash/src/web/ui/index.ts +56 -0
- package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
- package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
- package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
- package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
- package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
- package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
- package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
- package/bizar-dash/src/web/ui/layout/index.ts +33 -0
- package/bizar-dash/src/web/ui/layout/layout.css +413 -0
- package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
- package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
- package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
- package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
- package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
- package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
- package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
- package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
- package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
- package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
- package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
- package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
- package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
- package/bizar-dash/src/web/ui/styles/globals.css +86 -0
- package/bizar-dash/src/web/ui/styles/reset.css +125 -0
- package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
- package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
- package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
- package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
- package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
- package/bizar-dash/src/web/views/Agents.tsx +506 -266
- package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
- package/bizar-dash/src/web/views/Overview.tsx +996 -448
- package/bizar-dash/src/web/views/Tasks.tsx +277 -273
- package/bizar-dash/tests/communication-log.test.tsx +67 -0
- package/bizar-dash/tests/dep-graph.test.tsx +40 -0
- package/bizar-dash/tests/event-log.test.tsx +61 -0
- package/bizar-dash/tests/goal-input.test.tsx +72 -0
- package/bizar-dash/tests/goap-planner.test.ts +68 -0
- package/bizar-dash/tests/quality-gates.test.tsx +59 -0
- package/bizar-dash/tests/setup.ts +31 -1
- package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
- package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
- package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
- package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
- package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
- package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
- package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
- package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
- package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
- package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
- package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
- package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
- package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
- package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
- package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
- package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
- package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
- package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
- package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
- package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
- package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
- package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
- package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
- package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
- package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
- package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
- package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
- package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
- package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
- package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
- package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
- package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
- package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
- package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
- package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
- package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
- package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
- package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
- package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
- package/bizar-dash/tests/views/Agents.test.tsx +359 -0
- package/bizar-dash/tests/views/Overview.test.tsx +413 -0
- package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
- package/bizar-dash/vitest.config.ts +3 -1
- package/cli/__tests__/cost-gate.test.mjs +301 -0
- package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
- package/cli/bin.mjs +38 -0
- package/cli/commands/claim.mjs +282 -0
- package/cli/commands/cost.mjs +279 -0
- package/cli/cost-gate.mjs +501 -0
- package/cli/feature-list-bridge.mjs +400 -0
- package/cli/memory-constants.mjs +17 -0
- package/cli/worker-dispatcher.mjs +259 -0
- package/cli/worker-dispatcher.test.mjs +204 -0
- package/config/trigger-patterns.json +186 -0
- package/package.json +2 -2
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/agent-registry.ts +304 -0
- package/packages/sdk/src/consensus/byzantine.ts +587 -0
- package/packages/sdk/src/consensus/index.ts +147 -0
- package/packages/sdk/src/consensus/queen.ts +173 -0
- package/packages/sdk/src/consensus/types.ts +199 -0
- package/packages/sdk/src/federation/audit.ts +156 -0
- package/packages/sdk/src/federation/budget.ts +269 -0
- package/packages/sdk/src/federation/envelope.ts +144 -0
- package/packages/sdk/src/federation/hmac.ts +160 -0
- package/packages/sdk/src/federation/index.ts +443 -0
- package/packages/sdk/src/federation/pii.ts +241 -0
- package/packages/sdk/src/federation/policy.ts +112 -0
- package/packages/sdk/src/federation/trust.ts +146 -0
- package/packages/sdk/src/index.ts +67 -0
- package/packages/sdk/src/mcp/server.ts +351 -1
- package/packages/sdk/src/router/codemod-intent.ts +125 -0
- package/packages/sdk/src/router/index.ts +247 -0
- package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
- package/packages/sdk/src/router/memory-distillation.ts +249 -0
- package/packages/sdk/src/router/model-router.ts +235 -0
- package/packages/sdk/src/router/q-learning-router.ts +304 -0
- package/packages/sdk/src/swarm-topology.ts +279 -0
- package/packages/sdk/tests/agent-registry.test.ts +285 -0
- package/packages/sdk/tests/consensus.test.ts +376 -0
- package/packages/sdk/tests/federation/audit.test.ts +164 -0
- package/packages/sdk/tests/federation/budget.test.ts +189 -0
- package/packages/sdk/tests/federation/envelope.test.ts +120 -0
- package/packages/sdk/tests/federation/hmac.test.ts +194 -0
- package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
- package/packages/sdk/tests/federation/pii.test.ts +106 -0
- package/packages/sdk/tests/federation/policy.test.ts +140 -0
- package/packages/sdk/tests/federation/trust.test.ts +124 -0
- package/packages/sdk/tests/mcp-tools.test.ts +204 -0
- package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
- package/packages/sdk/tests/model-router.test.mjs +108 -0
- package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
- package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
- package/packages/sdk/tests/router.test.mjs +91 -0
- package/packages/sdk/tests/sdk.test.mjs +26 -2
- package/packages/sdk/tests/swarm-topology.test.ts +258 -0
- package/scripts/bh-full-e2e.mjs +11 -15
- package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
- package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
- package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
- package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
- package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
- package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
- package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
- package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
- package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
- package/packages/sdk/package-lock.json +0 -32
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/web/components/agents/DependencyGraph.tsx — DAG of plan step dependencies.
|
|
2
|
+
//
|
|
3
|
+
// v6.4.0 — F-036 (Goal Planner UI). Uses @xyflow/react (already in
|
|
4
|
+
// Bizar deps) to render the plan's step graph. Falls back to a
|
|
5
|
+
// hand-rolled table when no plan is supplied.
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
ReactFlow,
|
|
9
|
+
Background,
|
|
10
|
+
Controls,
|
|
11
|
+
MiniMap,
|
|
12
|
+
type Node,
|
|
13
|
+
type Edge,
|
|
14
|
+
type NodeProps,
|
|
15
|
+
Handle,
|
|
16
|
+
Position,
|
|
17
|
+
} from '@xyflow/react';
|
|
18
|
+
import '@xyflow/react/dist/style.css';
|
|
19
|
+
import { GitBranch } from 'lucide-react';
|
|
20
|
+
import { Card, CardTitle, CardMeta } from '../Card';
|
|
21
|
+
import { Tag } from '../Tag';
|
|
22
|
+
import type { Plan } from '../../lib/goapPlanner';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
plan: Plan | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function StepNode({ data }: NodeProps<Node<{ label: string; action: string; agent: string }>>) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`dep-node is-${(data as { action?: string }).action || 'research'}`}>
|
|
31
|
+
<Handle type="target" position={Position.Left} className="dep-handle" />
|
|
32
|
+
<div className="dep-node-action">{(data as { action?: string }).action}</div>
|
|
33
|
+
<div className="dep-node-label">{(data as { label?: string }).label}</div>
|
|
34
|
+
<div className="dep-node-agent">@{(data as { agent?: string }).agent}</div>
|
|
35
|
+
<Handle type="source" position={Position.Right} className="dep-handle" />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nodeTypes = { step: StepNode };
|
|
41
|
+
|
|
42
|
+
export function DependencyGraph({ plan }: Props) {
|
|
43
|
+
const nodes: Node[] = useMemo(() => {
|
|
44
|
+
if (!plan) return [];
|
|
45
|
+
const n = plan.steps.length;
|
|
46
|
+
// Lay out nodes in a horizontal flow based on dep depth.
|
|
47
|
+
const depths = new Map<string, number>();
|
|
48
|
+
const computeDepth = (id: string, stack = new Set<string>()): number => {
|
|
49
|
+
const cached = depths.get(id);
|
|
50
|
+
if (cached !== undefined) return cached;
|
|
51
|
+
if (stack.has(id)) return 0;
|
|
52
|
+
stack.add(id);
|
|
53
|
+
const step = plan.steps.find((s) => s.id === id);
|
|
54
|
+
if (!step || step.deps.length === 0) {
|
|
55
|
+
depths.set(id, 0);
|
|
56
|
+
stack.delete(id);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
let max = 0;
|
|
60
|
+
for (const dep of step.deps) {
|
|
61
|
+
const d = computeDepth(dep, stack);
|
|
62
|
+
if (d > max) max = d;
|
|
63
|
+
}
|
|
64
|
+
depths.set(id, max + 1);
|
|
65
|
+
stack.delete(id);
|
|
66
|
+
return max + 1;
|
|
67
|
+
};
|
|
68
|
+
for (const s of plan.steps) computeDepth(s.id);
|
|
69
|
+
|
|
70
|
+
const byDepth = new Map<number, string[]>();
|
|
71
|
+
for (const s of plan.steps) {
|
|
72
|
+
const d = depths.get(s.id) ?? 0;
|
|
73
|
+
const arr = byDepth.get(d) || [];
|
|
74
|
+
arr.push(s.id);
|
|
75
|
+
byDepth.set(d, arr);
|
|
76
|
+
}
|
|
77
|
+
const nodesOut: Node[] = [];
|
|
78
|
+
for (const [depth, ids] of byDepth) {
|
|
79
|
+
const total = ids.length;
|
|
80
|
+
ids.forEach((id, i) => {
|
|
81
|
+
const step = plan.steps.find((s) => s.id === id);
|
|
82
|
+
if (!step) return;
|
|
83
|
+
const xOffset = 60 + depth * 220;
|
|
84
|
+
const yOffset = 40 + (i - (total - 1) / 2) * 90;
|
|
85
|
+
nodesOut.push({
|
|
86
|
+
id: step.id,
|
|
87
|
+
type: 'step',
|
|
88
|
+
position: { x: xOffset, y: yOffset },
|
|
89
|
+
data: {
|
|
90
|
+
label: step.title,
|
|
91
|
+
action: step.action,
|
|
92
|
+
agent: step.agent,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
void n;
|
|
98
|
+
return nodesOut;
|
|
99
|
+
}, [plan]);
|
|
100
|
+
|
|
101
|
+
const edges: Edge[] = useMemo(() => {
|
|
102
|
+
if (!plan) return [];
|
|
103
|
+
return plan.steps.flatMap((step) =>
|
|
104
|
+
step.deps.map((dep) => ({
|
|
105
|
+
id: `${dep}->${step.id}`,
|
|
106
|
+
source: dep,
|
|
107
|
+
target: step.id,
|
|
108
|
+
animated: true,
|
|
109
|
+
})),
|
|
110
|
+
);
|
|
111
|
+
}, [plan]);
|
|
112
|
+
|
|
113
|
+
if (!plan) {
|
|
114
|
+
return (
|
|
115
|
+
<Card className="dep-graph" data-testid="dep-graph">
|
|
116
|
+
<CardTitle>
|
|
117
|
+
<GitBranch size={14} aria-hidden /> Dependency graph
|
|
118
|
+
</CardTitle>
|
|
119
|
+
<CardMeta>Submit a goal to render the dependency DAG.</CardMeta>
|
|
120
|
+
</Card>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (plan.steps.length === 0) {
|
|
125
|
+
return (
|
|
126
|
+
<Card className="dep-graph" data-testid="dep-graph">
|
|
127
|
+
<CardTitle>
|
|
128
|
+
<GitBranch size={14} aria-hidden /> Dependency graph
|
|
129
|
+
</CardTitle>
|
|
130
|
+
<CardMeta>Empty plan — nothing to render.</CardMeta>
|
|
131
|
+
</Card>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Card className="dep-graph" data-testid="dep-graph">
|
|
137
|
+
<div className="dep-graph-header">
|
|
138
|
+
<CardTitle>
|
|
139
|
+
<GitBranch size={14} aria-hidden /> Dependency graph
|
|
140
|
+
</CardTitle>
|
|
141
|
+
<Tag variant="info">{plan.steps.length} nodes</Tag>
|
|
142
|
+
<Tag variant="accent">{edges.length} edges</Tag>
|
|
143
|
+
</div>
|
|
144
|
+
<CardMeta>Plan step DAG · {plan.goal.slice(0, 80)}</CardMeta>
|
|
145
|
+
<div className="dep-graph-canvas" data-testid="dep-graph-canvas">
|
|
146
|
+
<ReactFlow
|
|
147
|
+
nodes={nodes}
|
|
148
|
+
edges={edges}
|
|
149
|
+
nodeTypes={nodeTypes}
|
|
150
|
+
fitView
|
|
151
|
+
fitViewOptions={{ padding: 0.15 }}
|
|
152
|
+
proOptions={{ hideAttribution: true }}
|
|
153
|
+
>
|
|
154
|
+
<Background gap={16} size={1} />
|
|
155
|
+
<Controls showInteractive={false} />
|
|
156
|
+
<MiniMap pannable zoomable />
|
|
157
|
+
</ReactFlow>
|
|
158
|
+
</div>
|
|
159
|
+
</Card>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default DependencyGraph;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/web/components/agents/QualityGates.tsx — pass/fail rubric display.
|
|
2
|
+
//
|
|
3
|
+
// v6.4.0 — F-036 (Goal Planner UI). Scores the four L09 rubric
|
|
4
|
+
// dimensions (Correctness / Arch compliance / Test coverage /
|
|
5
|
+
// Verification evidence) against the live plan. Each gate turns
|
|
6
|
+
// green when the supplied metric meets the threshold documented in
|
|
7
|
+
// `templates/evaluator-rubric.md`.
|
|
8
|
+
import { Shield, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react';
|
|
9
|
+
import { Card, CardTitle, CardMeta } from '../Card';
|
|
10
|
+
import { Tag } from '../Tag';
|
|
11
|
+
import type { Plan } from '../../lib/goapPlanner';
|
|
12
|
+
|
|
13
|
+
interface Metrics {
|
|
14
|
+
/** 0 or 1 — whether `make check` is green. */
|
|
15
|
+
compileCheck: boolean;
|
|
16
|
+
/** Percentage of touched tests passing. */
|
|
17
|
+
testCoverage: number;
|
|
18
|
+
/** Percentage of arch-rules passing. */
|
|
19
|
+
archScore: number;
|
|
20
|
+
/** Verification evidence (commit hash or test output) present. */
|
|
21
|
+
evidencePresent: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface GateRow {
|
|
25
|
+
name: string;
|
|
26
|
+
status: 'passed' | 'warning' | 'failed';
|
|
27
|
+
value?: number | boolean;
|
|
28
|
+
threshold?: number | boolean;
|
|
29
|
+
detail: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
plan: Plan | null;
|
|
34
|
+
metrics: Metrics;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function evaluate(metrics: Metrics): GateRow[] {
|
|
38
|
+
const compile: GateRow = metrics.compileCheck
|
|
39
|
+
? { name: 'Compile check', status: 'passed', value: true, threshold: true, detail: 'make check is green.' }
|
|
40
|
+
: { name: 'Compile check', status: 'failed', value: false, threshold: true, detail: 'Typecheck error — fix before marking passing.' };
|
|
41
|
+
|
|
42
|
+
const coverage: GateRow = metrics.testCoverage >= 80
|
|
43
|
+
? { name: 'Test coverage', status: 'passed', value: metrics.testCoverage, threshold: 80, detail: 'Coverage meets the B-grade threshold.' }
|
|
44
|
+
: metrics.testCoverage >= 60
|
|
45
|
+
? { name: 'Test coverage', status: 'warning', value: metrics.testCoverage, threshold: 80, detail: 'Coverage is below the B-grade threshold.' }
|
|
46
|
+
: { name: 'Test coverage', status: 'failed', value: metrics.testCoverage, threshold: 80, detail: 'Coverage is critical.' };
|
|
47
|
+
|
|
48
|
+
const arch: GateRow = metrics.archScore >= 90
|
|
49
|
+
? { name: 'Arch compliance', status: 'passed', value: metrics.archScore, threshold: 90, detail: 'All non-trivial arch-rules satisfied.' }
|
|
50
|
+
: metrics.archScore >= 70
|
|
51
|
+
? { name: 'Arch compliance', status: 'warning', value: metrics.archScore, threshold: 90, detail: 'Minor arch-rule violations.' }
|
|
52
|
+
: { name: 'Arch compliance', status: 'failed', value: metrics.archScore, threshold: 90, detail: 'Multiple arch-rule violations.' };
|
|
53
|
+
|
|
54
|
+
const evidence: GateRow = metrics.evidencePresent
|
|
55
|
+
? { name: 'Verification evidence', status: 'passed', value: true, threshold: true, detail: 'Test output + commit hash captured.' }
|
|
56
|
+
: { name: 'Verification evidence', status: 'failed', value: false, threshold: true, detail: 'No evidence recorded yet.' };
|
|
57
|
+
|
|
58
|
+
return [compile, coverage, arch, evidence];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function GateIcon({ status }: { status: GateRow['status'] }) {
|
|
62
|
+
if (status === 'passed') return <CheckCircle2 size={14} className="qg-icon is-pass" />;
|
|
63
|
+
if (status === 'warning') return <AlertTriangle size={14} className="qg-icon is-warn" />;
|
|
64
|
+
return <XCircle size={14} className="qg-icon is-fail" />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function GateTag({ status }: { status: GateRow['status'] }) {
|
|
68
|
+
if (status === 'passed') return <Tag variant="success">passed</Tag>;
|
|
69
|
+
if (status === 'warning') return <Tag variant="warning">warning</Tag>;
|
|
70
|
+
return <Tag variant="error">failed</Tag>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function QualityGates({ plan, metrics }: Props) {
|
|
74
|
+
const gates = evaluate(metrics);
|
|
75
|
+
const passing = gates.filter((g) => g.status === 'passed').length;
|
|
76
|
+
return (
|
|
77
|
+
<Card className="qg-panel" data-testid="quality-gates">
|
|
78
|
+
<div className="qg-header">
|
|
79
|
+
<CardTitle>
|
|
80
|
+
<Shield size={14} aria-hidden /> Quality gates
|
|
81
|
+
</CardTitle>
|
|
82
|
+
<Tag variant={passing === gates.length ? 'success' : passing > 0 ? 'warning' : 'error'}>
|
|
83
|
+
{passing}/{gates.length} passing
|
|
84
|
+
</Tag>
|
|
85
|
+
</div>
|
|
86
|
+
<CardMeta>
|
|
87
|
+
L09 rubric (Correctness / Arch compliance / Test coverage / Verification evidence) —{' '}
|
|
88
|
+
{plan ? `evaluating against plan with ${plan.steps.length} steps.` : 'awaiting plan.'}
|
|
89
|
+
</CardMeta>
|
|
90
|
+
<ul className="qg-list" data-testid="quality-gates-list">
|
|
91
|
+
{gates.map((gate) => (
|
|
92
|
+
<li
|
|
93
|
+
key={gate.name}
|
|
94
|
+
className={`qg-row is-${gate.status}`}
|
|
95
|
+
data-testid="quality-gates-row"
|
|
96
|
+
data-status={gate.status}
|
|
97
|
+
>
|
|
98
|
+
<div className="qg-row-head">
|
|
99
|
+
<GateIcon status={gate.status} />
|
|
100
|
+
<span className="qg-name">{gate.name}</span>
|
|
101
|
+
<GateTag status={gate.status} />
|
|
102
|
+
</div>
|
|
103
|
+
<div className="qg-row-body">
|
|
104
|
+
<span className="qg-detail">{gate.detail}</span>
|
|
105
|
+
{typeof gate.value === 'number' && (
|
|
106
|
+
<span className="qg-threshold">
|
|
107
|
+
current: {gate.value}% · threshold: {gate.threshold}%
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
{typeof gate.value === 'boolean' && (
|
|
111
|
+
<span className="qg-threshold">
|
|
112
|
+
current: {String(gate.value)} · threshold: {String(gate.threshold)}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</li>
|
|
117
|
+
))}
|
|
118
|
+
</ul>
|
|
119
|
+
</Card>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default QualityGates;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/web/components/agents/RealTimeEventLog.tsx — streaming event ticker.
|
|
2
|
+
//
|
|
3
|
+
// v6.4.0 — F-036 (Goal Planner UI). Subscribes to ws.ts for ALL
|
|
4
|
+
// events and renders a color-coded ticker. Bounded ring buffer so
|
|
5
|
+
// a runaway server can't fill memory.
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { Activity } from 'lucide-react';
|
|
8
|
+
import { Ws } from '../../lib/ws';
|
|
9
|
+
import { Card, CardTitle, CardMeta } from '../Card';
|
|
10
|
+
import { Tag } from '../Tag';
|
|
11
|
+
|
|
12
|
+
const MAX_ROWS = 500;
|
|
13
|
+
|
|
14
|
+
type EventRow = {
|
|
15
|
+
id: string;
|
|
16
|
+
ts: number;
|
|
17
|
+
type: string;
|
|
18
|
+
severity: 'info' | 'success' | 'warning' | 'error' | 'control';
|
|
19
|
+
preview: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function severityFor(type: string): EventRow['severity'] {
|
|
23
|
+
if (type.startsWith('goal:')) return 'success';
|
|
24
|
+
if (type.startsWith('task:')) return 'info';
|
|
25
|
+
if (type.includes('error') || type.includes('fail')) return 'error';
|
|
26
|
+
if (type.includes('stuck') || type.includes('blocked') || type.includes('warn')) return 'warning';
|
|
27
|
+
if (type === 'ping' || type === 'pong' || type === 'refresh') return 'control';
|
|
28
|
+
return 'info';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function previewOf(type: string, payload: Record<string, unknown> | undefined): string {
|
|
32
|
+
if (!payload) return type;
|
|
33
|
+
const keys = Object.keys(payload).slice(0, 3);
|
|
34
|
+
return keys.map((k) => `${k}=${shortVal(payload[k])}`).join(' ') || type;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shortVal(v: unknown): string {
|
|
38
|
+
if (v == null) return 'null';
|
|
39
|
+
if (typeof v === 'string') return v.length > 20 ? `"${v.slice(0, 20)}…"` : `"${v}"`;
|
|
40
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
41
|
+
if (Array.isArray(v)) return `[${v.length}]`;
|
|
42
|
+
if (typeof v === 'object') return `{${Object.keys(v).length}}`;
|
|
43
|
+
return String(v);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function RealTimeEventLog() {
|
|
47
|
+
const [rows, setRows] = useState<EventRow[]>([]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const ws = new Ws();
|
|
51
|
+
const off = ws.on((msg) => {
|
|
52
|
+
const m = msg as { type?: string; ts?: number } & Record<string, unknown>;
|
|
53
|
+
const t = m.type;
|
|
54
|
+
if (!t) return;
|
|
55
|
+
const ts = Number(m.ts) || Date.now();
|
|
56
|
+
const row: EventRow = {
|
|
57
|
+
id: `${ts}-${Math.random().toString(36).slice(2, 7)}`,
|
|
58
|
+
ts,
|
|
59
|
+
type: t,
|
|
60
|
+
severity: severityFor(t),
|
|
61
|
+
preview: previewOf(t, m as Record<string, unknown>),
|
|
62
|
+
};
|
|
63
|
+
setRows((prev) => {
|
|
64
|
+
const next = [...prev, row];
|
|
65
|
+
return next.length > MAX_ROWS ? next.slice(next.length - MAX_ROWS) : next;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
off();
|
|
70
|
+
ws.close();
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const summary = useMemo(() => {
|
|
75
|
+
const counts: Record<EventRow['severity'], number> = {
|
|
76
|
+
info: 0,
|
|
77
|
+
success: 0,
|
|
78
|
+
warning: 0,
|
|
79
|
+
error: 0,
|
|
80
|
+
control: 0,
|
|
81
|
+
};
|
|
82
|
+
for (const r of rows) counts[r.severity] += 1;
|
|
83
|
+
return counts;
|
|
84
|
+
}, [rows]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Card className="event-log" data-testid="event-log">
|
|
88
|
+
<div className="event-log-header">
|
|
89
|
+
<CardTitle>
|
|
90
|
+
<Activity size={14} aria-hidden /> Real-time events
|
|
91
|
+
</CardTitle>
|
|
92
|
+
<Tag variant="neutral" data-testid="event-log-count">{rows.length}</Tag>
|
|
93
|
+
</div>
|
|
94
|
+
<CardMeta>Live WebSocket event ticker · bounded to {MAX_ROWS} rows</CardMeta>
|
|
95
|
+
<div className="event-log-summary" data-testid="event-log-summary">
|
|
96
|
+
<Tag variant="info">info {summary.info}</Tag>
|
|
97
|
+
<Tag variant="success">success {summary.success}</Tag>
|
|
98
|
+
<Tag variant="warning">warning {summary.warning}</Tag>
|
|
99
|
+
<Tag variant="error">error {summary.error}</Tag>
|
|
100
|
+
<Tag variant="neutral">control {summary.control}</Tag>
|
|
101
|
+
</div>
|
|
102
|
+
<ul className="event-log-list" data-testid="event-log-list">
|
|
103
|
+
{rows.length === 0 && (
|
|
104
|
+
<li className="event-log-empty" data-testid="event-log-empty">
|
|
105
|
+
No events yet. Events will stream here as the server emits them.
|
|
106
|
+
</li>
|
|
107
|
+
)}
|
|
108
|
+
{rows.slice().reverse().map((row) => (
|
|
109
|
+
<li
|
|
110
|
+
key={row.id}
|
|
111
|
+
className={`event-log-row is-${row.severity}`}
|
|
112
|
+
data-testid="event-log-row"
|
|
113
|
+
data-severity={row.severity}
|
|
114
|
+
>
|
|
115
|
+
<time className="event-log-time" dateTime={new Date(row.ts).toISOString()}>
|
|
116
|
+
{new Date(row.ts).toLocaleTimeString()}
|
|
117
|
+
</time>
|
|
118
|
+
<code className="event-log-type">{row.type}</code>
|
|
119
|
+
<span className="event-log-preview">{row.preview}</span>
|
|
120
|
+
</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
</Card>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default RealTimeEventLog;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/web/components/agents/RoutingDecisions.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* F-035 — vitest tests for the routing-decisions panel.
|
|
5
|
+
* 3 tests required by the F-035 layers[] spec:
|
|
6
|
+
* 1. renders the empty state when no decisions have been emitted
|
|
7
|
+
* 2. renders a single decision via the `initial` prop
|
|
8
|
+
* 3. renders multiple decisions via the WS subscription path
|
|
9
|
+
*
|
|
10
|
+
* The `subscribe` prop receives a `(msg: WsMessage) => void` handler
|
|
11
|
+
* and returns an unsubscribe function. We capture the handler and
|
|
12
|
+
* drive it directly — no real WebSocket needed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
+
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { RoutingDecisions } from './RoutingDecisions';
|
|
19
|
+
import type { RoutingDecision, WsMessage } from '../../lib/types';
|
|
20
|
+
|
|
21
|
+
const dec = (id: string, tier: RoutingDecision['tier'], model: string, extras: Partial<RoutingDecision> = {}): RoutingDecision => ({
|
|
22
|
+
id,
|
|
23
|
+
ts: 1_700_000_000_000 + Number(id.replace(/\D/g, '') || 0) * 1000,
|
|
24
|
+
tier,
|
|
25
|
+
model,
|
|
26
|
+
task: `task-${id}`,
|
|
27
|
+
costUsd: tier === 'CODEMOD' ? 0 : 0.01,
|
|
28
|
+
latencyMs: 250,
|
|
29
|
+
...extras,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('RoutingDecisions', () => {
|
|
33
|
+
it('renders the empty state when no decisions have been emitted', () => {
|
|
34
|
+
render(<RoutingDecisions />);
|
|
35
|
+
const empty = screen.getByTestId('routing-decisions-empty');
|
|
36
|
+
expect(empty).toBeTruthy();
|
|
37
|
+
expect(empty.textContent).toMatch(/No routing decisions yet/i);
|
|
38
|
+
// Confirm table is NOT rendered in empty state.
|
|
39
|
+
expect(screen.queryByRole('table')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders a single decision via the initial prop', () => {
|
|
43
|
+
const one = dec('1', 'CODEMOD', 'codemod:var-to-const');
|
|
44
|
+
render(<RoutingDecisions initial={[one]} />);
|
|
45
|
+
|
|
46
|
+
// Tier badge with the right class
|
|
47
|
+
const row = screen.getByTestId(`routing-row-${one.id}`);
|
|
48
|
+
expect(row).toBeTruthy();
|
|
49
|
+
const badge = row.querySelector('.routing-tier-codemod');
|
|
50
|
+
expect(badge).toBeTruthy();
|
|
51
|
+
expect(badge?.textContent).toBe('CODEMOD');
|
|
52
|
+
|
|
53
|
+
// Model + task + cost columns
|
|
54
|
+
expect(row.querySelector('.routing-model')?.textContent).toBe('codemod:var-to-const');
|
|
55
|
+
expect(row.querySelector('.routing-task')?.textContent).toBe('task-1');
|
|
56
|
+
expect(row.querySelector('.routing-cost')?.textContent).toBe('$0');
|
|
57
|
+
// latency = 250ms → "250ms"
|
|
58
|
+
expect(row.querySelector('.routing-latency')?.textContent).toBe('250ms');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders multiple decisions and filters by tier', () => {
|
|
62
|
+
const list: RoutingDecision[] = [
|
|
63
|
+
dec('1', 'CODEMOD', 'codemod:remove-console', { costUsd: 0 }),
|
|
64
|
+
dec('2', 'TIER1', 'haiku', { costUsd: 0.0008 }),
|
|
65
|
+
dec('3', 'TIER2', 'sonnet', { costUsd: 0.012 }),
|
|
66
|
+
dec('4', 'TIER3', 'opus', { costUsd: 0.05 }),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const { rerender } = render(<RoutingDecisions initial={list} />);
|
|
70
|
+
// All four rows present
|
|
71
|
+
for (const d of list) {
|
|
72
|
+
expect(screen.getByTestId(`routing-row-${d.id}`)).toBeTruthy();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Tier filter — only TIER2 row survives
|
|
76
|
+
rerender(<RoutingDecisions initial={list} filterTier="TIER2" />);
|
|
77
|
+
expect(screen.getByTestId('routing-row-3')).toBeTruthy();
|
|
78
|
+
expect(screen.queryByTestId('routing-row-1')).toBeNull();
|
|
79
|
+
expect(screen.queryByTestId('routing-row-2')).toBeNull();
|
|
80
|
+
expect(screen.queryByTestId('routing-row-4')).toBeNull();
|
|
81
|
+
|
|
82
|
+
// Subscribed WS append test — drive a `routing:decision` through
|
|
83
|
+
// the captured handler.
|
|
84
|
+
let capturedHandler: ((msg: WsMessage) => void) | null = null;
|
|
85
|
+
const subscribe = (handler: (msg: WsMessage) => void) => {
|
|
86
|
+
capturedHandler = handler;
|
|
87
|
+
return () => { capturedHandler = null; };
|
|
88
|
+
};
|
|
89
|
+
const appended: RoutingDecision[] = [];
|
|
90
|
+
const onAppend = (d: RoutingDecision) => appended.push(d);
|
|
91
|
+
|
|
92
|
+
rerender(
|
|
93
|
+
<RoutingDecisions
|
|
94
|
+
initial={list}
|
|
95
|
+
subscribe={subscribe}
|
|
96
|
+
onAppend={onAppend}
|
|
97
|
+
filterTier="ALL"
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
expect(capturedHandler).toBeTruthy();
|
|
101
|
+
const fresh = dec('5', 'TIER3', 'opus-2', { costUsd: 0.07, latencyMs: 1200 });
|
|
102
|
+
// Drive the handler inside act() so React commits the setDecisions.
|
|
103
|
+
act(() => {
|
|
104
|
+
capturedHandler!({ type: 'routing:decision', decision: fresh });
|
|
105
|
+
});
|
|
106
|
+
expect(screen.getByTestId('routing-row-5')).toBeTruthy();
|
|
107
|
+
expect(appended).toHaveLength(1);
|
|
108
|
+
expect(appended[0].id).toBe('5');
|
|
109
|
+
|
|
110
|
+
// Malformed message should be ignored (no `routing:decision` type).
|
|
111
|
+
capturedHandler!({ type: 'tasks:change', task: {} as never });
|
|
112
|
+
expect(screen.queryByTestId('routing-row-')).toBeNull();
|
|
113
|
+
|
|
114
|
+
// Empty filterTier = 'ALL' shows everything
|
|
115
|
+
rerender(<RoutingDecisions initial={list} filterTier="ALL" />);
|
|
116
|
+
for (const d of list) {
|
|
117
|
+
expect(screen.getByTestId(`routing-row-${d.id}`)).toBeTruthy();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('respects maxRows by pruning oldest decisions', () => {
|
|
122
|
+
// The component treats `initial` as most-recent-first (same
|
|
123
|
+
// convention as the WS stream). Build the list newest-first so
|
|
124
|
+
// maxRows=3 keeps the last 3 items from the array (the newest).
|
|
125
|
+
const list: RoutingDecision[] = [
|
|
126
|
+
dec('5', 'TIER1', 'haiku'),
|
|
127
|
+
dec('4', 'TIER1', 'haiku'),
|
|
128
|
+
dec('3', 'TIER1', 'haiku'),
|
|
129
|
+
dec('2', 'TIER1', 'haiku'),
|
|
130
|
+
dec('1', 'TIER1', 'haiku'),
|
|
131
|
+
];
|
|
132
|
+
render(<RoutingDecisions initial={list} maxRows={3} />);
|
|
133
|
+
// Only 3 of 5 should be visible (most-recent first)
|
|
134
|
+
expect(screen.getByTestId('routing-row-5')).toBeTruthy();
|
|
135
|
+
expect(screen.getByTestId('routing-row-4')).toBeTruthy();
|
|
136
|
+
expect(screen.getByTestId('routing-row-3')).toBeTruthy();
|
|
137
|
+
expect(screen.queryByTestId('routing-row-2')).toBeNull();
|
|
138
|
+
expect(screen.queryByTestId('routing-row-1')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('compact prop toggles a different class', () => {
|
|
142
|
+
const one = dec('1', 'CODEMOD', 'codemod:add-logging');
|
|
143
|
+
const { container, rerender } = render(<RoutingDecisions initial={[one]} />);
|
|
144
|
+
expect(container.querySelector('.routing-decisions-compact')).toBeNull();
|
|
145
|
+
rerender(<RoutingDecisions initial={[one]} compact />);
|
|
146
|
+
expect(container.querySelector('.routing-decisions-compact')).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
});
|