agentic-orchestrator 0.1.25 → 0.1.26

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.
Files changed (177) hide show
  1. package/agentic/orchestrator/defaults/policy.defaults.yaml +1 -0
  2. package/agentic/orchestrator/policy.yaml +1 -0
  3. package/agentic/orchestrator/schemas/policy.schema.json +4 -0
  4. package/agentic/orchestrator/schemas/policy.user.schema.json +4 -0
  5. package/apps/control-plane/src/application/services/activity-monitor-service.ts +42 -0
  6. package/apps/control-plane/src/application/services/collision-queue-service.ts +48 -0
  7. package/apps/control-plane/src/application/services/cost-tracking-service.ts +80 -0
  8. package/apps/control-plane/src/application/services/feature-deletion-service.ts +57 -0
  9. package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +65 -0
  10. package/apps/control-plane/src/application/services/feature-state-service.ts +87 -0
  11. package/apps/control-plane/src/application/services/gate-selection-service.ts +28 -0
  12. package/apps/control-plane/src/application/services/gate-service.ts +148 -0
  13. package/apps/control-plane/src/application/services/issue-tracker-service.ts +28 -0
  14. package/apps/control-plane/src/application/services/lock-service.ts +88 -0
  15. package/apps/control-plane/src/application/services/merge-service.ts +63 -0
  16. package/apps/control-plane/src/application/services/notifier-service.ts +70 -0
  17. package/apps/control-plane/src/application/services/patch-service.ts +95 -0
  18. package/apps/control-plane/src/application/services/performance-analytics-service.ts +90 -0
  19. package/apps/control-plane/src/application/services/plan-service.ts +184 -0
  20. package/apps/control-plane/src/application/services/pr-monitor-service.ts +58 -0
  21. package/apps/control-plane/src/application/services/qa-index-service.ts +68 -0
  22. package/apps/control-plane/src/application/services/reactions-service.ts +77 -0
  23. package/apps/control-plane/src/application/services/reporting-service.ts +79 -0
  24. package/apps/control-plane/src/application/services/run-lease-service.ts +79 -0
  25. package/apps/control-plane/src/application/tools/tool-router.ts +76 -0
  26. package/apps/control-plane/src/mcp/tool-runtime.ts +4 -0
  27. package/apps/control-plane/src/providers/providers.ts +96 -0
  28. package/apps/control-plane/src/providers/worker-provider-factory.ts +4 -0
  29. package/apps/control-plane/src/supervisor/build-wave-executor.ts +4 -0
  30. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +4 -0
  31. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +4 -0
  32. package/apps/control-plane/src/supervisor/run-coordinator.ts +4 -0
  33. package/apps/control-plane/src/supervisor/runtime.ts +4 -0
  34. package/apps/control-plane/src/supervisor/session-orchestrator.ts +4 -0
  35. package/apps/control-plane/test/dashboard-api.integration.spec.ts +310 -1
  36. package/apps/control-plane/test/dashboard-client.spec.ts +136 -1
  37. package/apps/control-plane/test/dashboard-live-feed.spec.ts +153 -0
  38. package/apps/control-plane/test/dashboard-ui-utils.spec.ts +132 -0
  39. package/apps/control-plane/test/mcp.spec.ts +96 -0
  40. package/apps/control-plane/test/tool-runtime.spec.ts +68 -0
  41. package/config/agentic/orchestrator/policy.yaml +1 -0
  42. package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +42 -0
  43. package/dist/apps/control-plane/application/services/activity-monitor-service.js +42 -0
  44. package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
  45. package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +48 -0
  46. package/dist/apps/control-plane/application/services/collision-queue-service.js +48 -0
  47. package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
  48. package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +79 -0
  49. package/dist/apps/control-plane/application/services/cost-tracking-service.js +76 -0
  50. package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
  51. package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +57 -0
  52. package/dist/apps/control-plane/application/services/feature-deletion-service.js +57 -0
  53. package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
  54. package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +64 -0
  55. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +61 -0
  56. package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
  57. package/dist/apps/control-plane/application/services/feature-state-service.d.ts +86 -0
  58. package/dist/apps/control-plane/application/services/feature-state-service.js +83 -0
  59. package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
  60. package/dist/apps/control-plane/application/services/gate-selection-service.d.ts +28 -0
  61. package/dist/apps/control-plane/application/services/gate-selection-service.js +28 -0
  62. package/dist/apps/control-plane/application/services/gate-selection-service.js.map +1 -1
  63. package/dist/apps/control-plane/application/services/gate-service.d.ts +148 -0
  64. package/dist/apps/control-plane/application/services/gate-service.js +120 -0
  65. package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
  66. package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +28 -0
  67. package/dist/apps/control-plane/application/services/issue-tracker-service.js +28 -0
  68. package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -1
  69. package/dist/apps/control-plane/application/services/lock-service.d.ts +88 -0
  70. package/dist/apps/control-plane/application/services/lock-service.js +64 -0
  71. package/dist/apps/control-plane/application/services/lock-service.js.map +1 -1
  72. package/dist/apps/control-plane/application/services/merge-service.d.ts +62 -0
  73. package/dist/apps/control-plane/application/services/merge-service.js +59 -0
  74. package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
  75. package/dist/apps/control-plane/application/services/notifier-service.d.ts +70 -0
  76. package/dist/apps/control-plane/application/services/notifier-service.js +70 -0
  77. package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
  78. package/dist/apps/control-plane/application/services/patch-service.d.ts +94 -0
  79. package/dist/apps/control-plane/application/services/patch-service.js +91 -0
  80. package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
  81. package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +89 -0
  82. package/dist/apps/control-plane/application/services/performance-analytics-service.js +86 -0
  83. package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
  84. package/dist/apps/control-plane/application/services/plan-service.d.ts +287 -0
  85. package/dist/apps/control-plane/application/services/plan-service.js +207 -0
  86. package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
  87. package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +58 -0
  88. package/dist/apps/control-plane/application/services/pr-monitor-service.js +58 -0
  89. package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -1
  90. package/dist/apps/control-plane/application/services/qa-index-service.d.ts +67 -0
  91. package/dist/apps/control-plane/application/services/qa-index-service.js +64 -0
  92. package/dist/apps/control-plane/application/services/qa-index-service.js.map +1 -1
  93. package/dist/apps/control-plane/application/services/reactions-service.d.ts +76 -0
  94. package/dist/apps/control-plane/application/services/reactions-service.js +73 -0
  95. package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
  96. package/dist/apps/control-plane/application/services/reporting-service.d.ts +78 -0
  97. package/dist/apps/control-plane/application/services/reporting-service.js +75 -0
  98. package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
  99. package/dist/apps/control-plane/application/services/run-lease-service.d.ts +79 -0
  100. package/dist/apps/control-plane/application/services/run-lease-service.js +79 -0
  101. package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
  102. package/dist/apps/control-plane/application/tools/tool-router.d.ts +76 -0
  103. package/dist/apps/control-plane/application/tools/tool-router.js +62 -0
  104. package/dist/apps/control-plane/application/tools/tool-router.js.map +1 -1
  105. package/dist/apps/control-plane/mcp/tool-runtime.d.ts +3 -0
  106. package/dist/apps/control-plane/mcp/tool-runtime.js +3 -0
  107. package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
  108. package/dist/apps/control-plane/providers/providers.d.ts +88 -0
  109. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  110. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +3 -0
  111. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
  112. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
  113. package/dist/apps/control-plane/supervisor/build-wave-executor.js +3 -0
  114. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  115. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +3 -0
  116. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +3 -0
  117. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  118. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
  119. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +3 -0
  120. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  121. package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
  122. package/dist/apps/control-plane/supervisor/run-coordinator.js +3 -0
  123. package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
  124. package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
  125. package/dist/apps/control-plane/supervisor/runtime.js +3 -0
  126. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  127. package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +3 -0
  128. package/dist/apps/control-plane/supervisor/session-orchestrator.js +3 -0
  129. package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
  130. package/package.json +1 -1
  131. package/packages/web-dashboard/src/app/analytics/page.tsx +451 -0
  132. package/packages/web-dashboard/src/app/api/analytics/route.ts +62 -0
  133. package/packages/web-dashboard/src/app/api/collisions/route.ts +63 -0
  134. package/packages/web-dashboard/src/app/api/features/[id]/cost/route.ts +29 -0
  135. package/packages/web-dashboard/src/app/api/features/[id]/review-brief/route.ts +31 -0
  136. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +5 -8
  137. package/packages/web-dashboard/src/app/api/features/[id]/test-index/route.ts +31 -0
  138. package/packages/web-dashboard/src/app/api/flaky/route.ts +20 -0
  139. package/packages/web-dashboard/src/app/api/policy/budget/route.ts +31 -0
  140. package/packages/web-dashboard/src/app/api/run/route.ts +162 -0
  141. package/packages/web-dashboard/src/app/api/status/route.ts +2 -5
  142. package/packages/web-dashboard/src/app/feature/[id]/page.tsx +112 -0
  143. package/packages/web-dashboard/src/app/page.tsx +379 -77
  144. package/packages/web-dashboard/src/components/agent-pipeline-stepper.tsx +100 -0
  145. package/packages/web-dashboard/src/components/dependency-chains.tsx +58 -0
  146. package/packages/web-dashboard/src/components/detail-panel.tsx +215 -5
  147. package/packages/web-dashboard/src/components/feature-card.tsx +5 -0
  148. package/packages/web-dashboard/src/components/feature-cost-panel.tsx +66 -0
  149. package/packages/web-dashboard/src/components/feature-list-view.tsx +336 -0
  150. package/packages/web-dashboard/src/components/gate-step-drilldown.tsx +92 -0
  151. package/packages/web-dashboard/src/components/kanban-board.tsx +3 -0
  152. package/packages/web-dashboard/src/components/live-event-feed.tsx +60 -0
  153. package/packages/web-dashboard/src/components/lock-resource-map.tsx +73 -0
  154. package/packages/web-dashboard/src/components/plan-revision-timeline.tsx +55 -0
  155. package/packages/web-dashboard/src/components/plan-risk-annotations.tsx +63 -0
  156. package/packages/web-dashboard/src/components/plan-scope-tree.tsx +285 -0
  157. package/packages/web-dashboard/src/components/qa-coverage-map.tsx +174 -0
  158. package/packages/web-dashboard/src/components/quick-launch-panel.tsx +70 -0
  159. package/packages/web-dashboard/src/components/review-brief-panel.tsx +75 -0
  160. package/packages/web-dashboard/src/components/run-health-panel.tsx +85 -0
  161. package/packages/web-dashboard/src/components/summary-bar.tsx +59 -2
  162. package/packages/web-dashboard/src/components/verification-signals.tsx +167 -0
  163. package/packages/web-dashboard/src/lib/analytics-utils.ts +3 -0
  164. package/packages/web-dashboard/src/lib/aop-client.ts +510 -11
  165. package/packages/web-dashboard/src/lib/api-envelope.ts +48 -0
  166. package/packages/web-dashboard/src/lib/authz-adapter.ts +33 -0
  167. package/packages/web-dashboard/src/lib/dashboard-utils.ts +304 -0
  168. package/packages/web-dashboard/src/lib/live-feed.ts +218 -0
  169. package/packages/web-dashboard/src/lib/policy-reader.ts +22 -0
  170. package/packages/web-dashboard/src/lib/types.ts +114 -0
  171. package/packages/web-dashboard/src/styles/dashboard.module.css +341 -1
  172. package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_advanced_ux_spec.md +1 -1
  173. package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +561 -0
  174. package/spec-files/outstanding/agentic_orchestrator_kernel_simplification_spec.md +834 -0
  175. package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +12 -5
  176. package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +442 -0
  177. package/spec-files/progress.md +174 -1
@@ -1,14 +1,26 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { ConfirmationModal } from '@/components/confirmation-modal';
4
6
  import { CollisionQueue } from '@/components/collision-queue';
5
7
  import { DetailPanel } from '@/components/detail-panel';
8
+ import { DependencyChains } from '@/components/dependency-chains';
9
+ import { FeatureListView } from '@/components/feature-list-view';
6
10
  import { FilterBar } from '@/components/filter-bar';
7
11
  import { KanbanBoard } from '@/components/kanban-board';
12
+ import { LiveEventFeed } from '@/components/live-event-feed';
13
+ import { LockResourceMap } from '@/components/lock-resource-map';
14
+ import { QuickLaunchPanel } from '@/components/quick-launch-panel';
15
+ import { RunHealthPanel } from '@/components/run-health-panel';
8
16
  import { SummaryBar } from '@/components/summary-bar';
9
17
  import { ToastStack, useToast } from '@/components/toast';
10
18
  import {
19
+ applyDashboardRouteState,
11
20
  applyQuickFilter,
21
+ getNextFocusedFeatureId,
22
+ isQuickLaunchAffordanceVisible,
23
+ parseDashboardRouteState,
12
24
  COLUMN_ORDER,
13
25
  computeSummaryCounts,
14
26
  createDefaultFilters,
@@ -22,10 +34,24 @@ import type {
22
34
  FeatureSummary,
23
35
  SSEEvent,
24
36
  } from '@/lib/types.js';
37
+ import {
38
+ appendFeedEvents,
39
+ deriveFeedEventsFromSnapshotDiff,
40
+ reduceLiveFeedState,
41
+ type LiveFeedState,
42
+ } from '@/lib/live-feed.js';
25
43
  import styles from '@/styles/dashboard.module.css';
26
44
 
27
45
  type ProjectEntry = { name: string; path: string };
28
46
  type ProjectDefaults = { default_project?: string | null } | null;
47
+ type QuickLaunchAvailability = {
48
+ available: boolean;
49
+ enabled: boolean;
50
+ authorized: boolean;
51
+ reason: string | null;
52
+ };
53
+
54
+ const FEATURE_ID_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{1,126}[a-z0-9])?$/;
29
55
 
30
56
  async function jsonFetch<T>(input: string, init?: RequestInit): Promise<T> {
31
57
  const response = await fetch(input, init);
@@ -43,18 +69,69 @@ function sortFeatures(features: FeatureSummary[]): FeatureSummary[] {
43
69
  return [...features].sort((left, right) => left.feature_id.localeCompare(right.feature_id));
44
70
  }
45
71
 
72
+ function createInitialFeedState(): LiveFeedState {
73
+ return { events: [], paused: false };
74
+ }
75
+
46
76
  export default function Dashboard() {
47
77
  const [payload, setPayload] = useState<DashboardStatusPayload>(emptyStatusPayload());
48
78
  const [connected, setConnected] = useState(false);
49
79
  const [lastSnapshotAt, setLastSnapshotAt] = useState<number | null>(null);
80
+ const [nowMs, setNowMs] = useState(Date.now());
81
+ const [viewMode, setViewMode] = useState<'board' | 'list'>('board');
50
82
  const [selectedFeatureId, setSelectedFeatureId] = useState<string | null>(null);
51
83
  const [focusedFeatureId, setFocusedFeatureId] = useState<string | null>(null);
52
84
  const [detail, setDetail] = useState<FeatureDetail | null>(null);
53
85
  const [projects, setProjects] = useState<ProjectEntry[]>([]);
54
86
  const [project, setProject] = useState('');
55
87
  const [filters, setFilters] = useState(createDefaultFilters());
88
+ const [feedState, setFeedState] = useState<LiveFeedState>(createInitialFeedState());
89
+ const [quickLaunchAvailability, setQuickLaunchAvailability] = useState<QuickLaunchAvailability>({
90
+ available: false,
91
+ enabled: false,
92
+ authorized: false,
93
+ reason: null,
94
+ });
95
+ const [quickLaunchFeatureId, setQuickLaunchFeatureId] = useState('');
96
+ const [quickLaunchGoal, setQuickLaunchGoal] = useState('');
97
+ const [quickLaunchSubmitting, setQuickLaunchSubmitting] = useState(false);
98
+ const [quickLaunchConfirmOpen, setQuickLaunchConfirmOpen] = useState(false);
56
99
  const { notifications, addToast, dismissToast } = useToast();
57
100
  const searchInputRef = useRef<HTMLInputElement>(null);
101
+ const previousPayloadRef = useRef<DashboardStatusPayload | null>(null);
102
+ const feedPausedRef = useRef(false);
103
+
104
+ const syncUrlState = useCallback(
105
+ (
106
+ options: {
107
+ featureId?: string | null;
108
+ view?: 'board' | 'list';
109
+ projectName?: string;
110
+ nextFilters?: ReturnType<typeof createDefaultFilters>;
111
+ },
112
+ method: 'push' | 'replace' = 'push',
113
+ ) => {
114
+ const url = new URL(window.location.href);
115
+ const nextFeature = options.featureId !== undefined ? options.featureId : selectedFeatureId;
116
+ const nextView = options.view ?? viewMode;
117
+ const nextProject = options.projectName !== undefined ? options.projectName : project;
118
+ const nextFilters = options.nextFilters ?? filters;
119
+
120
+ applyDashboardRouteState(url.searchParams, {
121
+ project: nextProject,
122
+ featureId: nextFeature,
123
+ viewMode: nextView,
124
+ filters: nextFilters,
125
+ });
126
+
127
+ if (method === 'replace') {
128
+ window.history.replaceState(null, '', url.toString());
129
+ } else {
130
+ window.history.pushState(null, '', url.toString());
131
+ }
132
+ },
133
+ [filters, project, selectedFeatureId, viewMode],
134
+ );
58
135
 
59
136
  const refreshDetail = useCallback(
60
137
  async (featureId: string) => {
@@ -74,6 +151,7 @@ export default function Dashboard() {
74
151
  );
75
152
  if (response.ok && response.data) {
76
153
  setPayload(response.data);
154
+ previousPayloadRef.current = response.data;
77
155
  }
78
156
  }, [project]);
79
157
 
@@ -105,18 +183,73 @@ export default function Dashboard() {
105
183
 
106
184
  useEffect(() => {
107
185
  const params = new URLSearchParams(window.location.search);
108
- const queryProject = params.get('project');
109
- if (queryProject) {
110
- setProject(queryProject);
186
+ const routeState = parseDashboardRouteState(params);
187
+ if (params.has('project')) {
188
+ setProject(routeState.project);
111
189
  }
190
+ setSelectedFeatureId(routeState.featureId);
191
+ setFocusedFeatureId(routeState.featureId);
192
+ setViewMode(routeState.viewMode);
193
+ setFilters(routeState.filters);
194
+ }, []);
195
+
196
+ useEffect(() => {
197
+ const onPopState = () => {
198
+ const routeState = parseDashboardRouteState(new URLSearchParams(window.location.search));
199
+ setProject(routeState.project);
200
+ setViewMode(routeState.viewMode);
201
+ setSelectedFeatureId(routeState.featureId);
202
+ setFocusedFeatureId(routeState.featureId);
203
+ setFilters(routeState.filters);
204
+ };
205
+ window.addEventListener('popstate', onPopState);
206
+ return () => window.removeEventListener('popstate', onPopState);
112
207
  }, []);
113
208
 
114
209
  useEffect(() => {
115
210
  setSelectedFeatureId(null);
116
211
  setFocusedFeatureId(null);
117
212
  setDetail(null);
213
+ setFeedState(createInitialFeedState());
214
+ previousPayloadRef.current = null;
215
+ }, [project]);
216
+
217
+ useEffect(() => {
218
+ feedPausedRef.current = feedState.paused;
219
+ }, [feedState.paused]);
220
+
221
+ const refreshQuickLaunchAvailability = useCallback(async () => {
222
+ try {
223
+ const response = await jsonFetch<{
224
+ ok: boolean;
225
+ data?: { available?: boolean; enabled?: boolean; authorized?: boolean; reason?: string };
226
+ }>(`/api/run${projectQuery(project)}`);
227
+
228
+ if (response.ok && response.data) {
229
+ setQuickLaunchAvailability({
230
+ available: response.data.available === true,
231
+ enabled: response.data.enabled === true,
232
+ authorized: response.data.authorized === true,
233
+ reason: typeof response.data.reason === 'string' ? response.data.reason : null,
234
+ });
235
+ return;
236
+ }
237
+ } catch {
238
+ // fallback below
239
+ }
240
+
241
+ setQuickLaunchAvailability({
242
+ available: false,
243
+ enabled: false,
244
+ authorized: false,
245
+ reason: null,
246
+ });
118
247
  }, [project]);
119
248
 
249
+ useEffect(() => {
250
+ void refreshQuickLaunchAvailability();
251
+ }, [refreshQuickLaunchAvailability]);
252
+
120
253
  useEffect(() => {
121
254
  setConnected(false);
122
255
  setLastSnapshotAt(null);
@@ -130,6 +263,20 @@ export default function Dashboard() {
130
263
  try {
131
264
  const parsed = JSON.parse(event.data) as SSEEvent;
132
265
  if (parsed.type === 'snapshot' && parsed.payload) {
266
+ const nextPayload = parsed.payload;
267
+ if (!feedPausedRef.current) {
268
+ const derived = deriveFeedEventsFromSnapshotDiff(
269
+ previousPayloadRef.current,
270
+ nextPayload,
271
+ );
272
+ if (derived.length > 0) {
273
+ setFeedState((current) => ({
274
+ ...current,
275
+ events: appendFeedEvents(current.events, derived),
276
+ }));
277
+ }
278
+ }
279
+ previousPayloadRef.current = nextPayload;
133
280
  setPayload(parsed.payload);
134
281
  setConnected(true);
135
282
  setLastSnapshotAt(Date.now());
@@ -147,12 +294,14 @@ export default function Dashboard() {
147
294
 
148
295
  useEffect(() => {
149
296
  const interval = setInterval(() => {
297
+ const now = Date.now();
298
+ setNowMs(now);
150
299
  if (lastSnapshotAt == null) {
151
300
  setConnected(false);
152
301
  return;
153
302
  }
154
- setConnected(Date.now() - lastSnapshotAt < 20_000);
155
- }, 1000);
303
+ setConnected(now - lastSnapshotAt < 20_000);
304
+ }, 5000);
156
305
 
157
306
  return () => clearInterval(interval);
158
307
  }, [lastSnapshotAt]);
@@ -248,67 +397,30 @@ export default function Dashboard() {
248
397
  }
249
398
 
250
399
  const focusId = focusedFeatureId ?? allVisibleFeatureIds[0];
251
- const currentColumnIndex = columnFeatureIds.findIndex((column) =>
252
- column.featureIds.includes(focusId),
253
- );
254
- const currentColumn =
255
- currentColumnIndex >= 0 ? columnFeatureIds[currentColumnIndex] : columnFeatureIds[0];
256
- const currentRow = Math.max(0, currentColumn.featureIds.indexOf(focusId));
257
-
258
- const moveWithinColumn = (offset: number) => {
259
- const nextRow = Math.min(
260
- currentColumn.featureIds.length - 1,
261
- Math.max(0, currentRow + offset),
262
- );
263
- const nextFeatureId = currentColumn.featureIds[nextRow];
264
- if (nextFeatureId) {
265
- setFocusedFeatureId(nextFeatureId);
266
- }
267
- };
268
-
269
- const moveAcrossColumns = (direction: 1 | -1) => {
270
- let cursor = currentColumnIndex;
271
- while (cursor >= 0 && cursor < columnFeatureIds.length) {
272
- cursor += direction;
273
- if (cursor < 0 || cursor >= columnFeatureIds.length) {
274
- break;
275
- }
276
- const candidate = columnFeatureIds[cursor];
277
- if (candidate.featureIds.length === 0) {
278
- continue;
279
- }
280
- const nextRow = Math.min(currentRow, candidate.featureIds.length - 1);
281
- const nextFeatureId = candidate.featureIds[nextRow];
282
- if (nextFeatureId) {
283
- setFocusedFeatureId(nextFeatureId);
284
- }
285
- break;
286
- }
287
- };
288
-
289
- if (event.key === 'ArrowUp') {
290
- event.preventDefault();
291
- moveWithinColumn(-1);
292
- } else if (event.key === 'ArrowDown') {
293
- event.preventDefault();
294
- moveWithinColumn(1);
295
- } else if (event.key === 'ArrowLeft') {
296
- event.preventDefault();
297
- moveAcrossColumns(-1);
298
- } else if (event.key === 'ArrowRight') {
400
+ if (
401
+ event.key === 'ArrowUp' ||
402
+ event.key === 'ArrowDown' ||
403
+ event.key === 'ArrowLeft' ||
404
+ event.key === 'ArrowRight'
405
+ ) {
299
406
  event.preventDefault();
300
- moveAcrossColumns(1);
407
+ const arrowKey = event.key as 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
408
+ const nextFocus = getNextFocusedFeatureId(columnFeatureIds, focusId, arrowKey);
409
+ if (nextFocus) {
410
+ setFocusedFeatureId(nextFocus);
411
+ }
301
412
  } else if (event.key === 'Enter') {
302
413
  event.preventDefault();
303
414
  if (focusId) {
304
415
  setSelectedFeatureId(focusId);
416
+ syncUrlState({ featureId: focusId });
305
417
  }
306
418
  }
307
419
  };
308
420
 
309
421
  window.addEventListener('keydown', onKeyDown);
310
422
  return () => window.removeEventListener('keydown', onKeyDown);
311
- }, [allVisibleFeatureIds, columnFeatureIds, focusedFeatureId]);
423
+ }, [allVisibleFeatureIds, columnFeatureIds, focusedFeatureId, syncUrlState]);
312
424
 
313
425
  useEffect(() => {
314
426
  if (!selectedFeatureId) {
@@ -319,18 +431,113 @@ export default function Dashboard() {
319
431
  if (!exists) {
320
432
  setSelectedFeatureId(null);
321
433
  setDetail(null);
434
+ syncUrlState({ featureId: null }, 'replace');
322
435
  }
323
- }, [payload.features, selectedFeatureId]);
436
+ }, [payload.features, selectedFeatureId, syncUrlState]);
324
437
 
325
438
  const summaryCounts = useMemo(() => computeSummaryCounts(payload), [payload]);
326
439
  const hasActiveFilters = filtersAreActive(filters);
440
+ const dependencyChains = useMemo(
441
+ () =>
442
+ Array.isArray(payload.dep_blocked)
443
+ ? payload.dep_blocked
444
+ .filter((item) => item.depends_on_unresolved.length > 0)
445
+ .sort((left, right) => left.feature_id.localeCompare(right.feature_id))
446
+ : [],
447
+ [payload.dep_blocked],
448
+ );
449
+ const dependencyMap = useMemo(() => {
450
+ const map = new Map<string, string[]>();
451
+ for (const entry of dependencyChains) {
452
+ map.set(entry.feature_id, entry.depends_on_unresolved);
453
+ }
454
+ return map;
455
+ }, [dependencyChains]);
456
+ const dependencyBlockedIds = useMemo(
457
+ () => new Set(dependencyChains.map((entry) => entry.feature_id)),
458
+ [dependencyChains],
459
+ );
460
+ const selectedDependencies = useMemo(() => {
461
+ if (!selectedFeatureId) {
462
+ return [];
463
+ }
464
+ return dependencyMap.get(selectedFeatureId) ?? [];
465
+ }, [dependencyMap, selectedFeatureId]);
466
+
467
+ const applyFilters = (
468
+ next: ReturnType<typeof createDefaultFilters>,
469
+ method: 'push' | 'replace' = 'replace',
470
+ ) => {
471
+ setFilters(next);
472
+ syncUrlState({ nextFilters: next }, method);
473
+ };
327
474
 
328
475
  const setQuickFilter = (key: Parameters<typeof applyQuickFilter>[0]) => {
329
476
  if (filters.quickFilter === key) {
330
- setFilters(createDefaultFilters());
477
+ applyFilters(createDefaultFilters(), 'push');
478
+ return;
479
+ }
480
+ applyFilters(applyQuickFilter(key), 'push');
481
+ };
482
+
483
+ const requestQuickLaunch = () => {
484
+ const featureId = quickLaunchFeatureId.trim();
485
+ const goal = quickLaunchGoal.trim();
486
+ if (!FEATURE_ID_PATTERN.test(featureId)) {
487
+ addToast('feature_id must match /^[a-z0-9](?:[a-z0-9_-]{1,126}[a-z0-9])?$/', 'error');
488
+ return;
489
+ }
490
+ if (goal.length === 0) {
491
+ addToast('Goal is required', 'error');
331
492
  return;
332
493
  }
333
- setFilters(applyQuickFilter(key));
494
+ setQuickLaunchConfirmOpen(true);
495
+ };
496
+
497
+ const confirmQuickLaunch = async () => {
498
+ if (quickLaunchSubmitting) {
499
+ return;
500
+ }
501
+ setQuickLaunchSubmitting(true);
502
+ try {
503
+ const response = await fetch(`/api/run${projectQuery(project)}`, {
504
+ method: 'POST',
505
+ headers: { 'Content-Type': 'application/json' },
506
+ body: JSON.stringify({
507
+ feature_id: quickLaunchFeatureId.trim(),
508
+ goal: quickLaunchGoal.trim(),
509
+ confirm_launch: true,
510
+ }),
511
+ });
512
+ const body = (await response.json()) as {
513
+ ok: boolean;
514
+ data?: { feature_id: string };
515
+ error?: { message?: string; code?: string };
516
+ };
517
+
518
+ if (!body.ok || !body.data) {
519
+ addToast(body.error?.message ?? 'Quick launch failed', 'error');
520
+ if (response.status === 403) {
521
+ await refreshQuickLaunchAvailability();
522
+ }
523
+ return;
524
+ }
525
+
526
+ addToast(`Launched ${body.data.feature_id}`, 'success');
527
+ setQuickLaunchFeatureId('');
528
+ setQuickLaunchGoal('');
529
+ setSelectedFeatureId(body.data.feature_id);
530
+ setFocusedFeatureId(body.data.feature_id);
531
+ syncUrlState({ featureId: body.data.feature_id });
532
+ await refreshStatus();
533
+ await refreshDetail(body.data.feature_id);
534
+ await refreshQuickLaunchAvailability();
535
+ } catch {
536
+ addToast('Quick launch failed', 'error');
537
+ } finally {
538
+ setQuickLaunchSubmitting(false);
539
+ setQuickLaunchConfirmOpen(false);
540
+ }
334
541
  };
335
542
 
336
543
  return (
@@ -344,6 +551,32 @@ export default function Dashboard() {
344
551
  </div>
345
552
 
346
553
  <div className={styles.metaControls}>
554
+ <Link href={`/analytics${projectQuery(project)}`} className={styles.linkButton}>
555
+ Analytics
556
+ </Link>
557
+ <div className={styles.inlineActions}>
558
+ <button
559
+ type="button"
560
+ className={`${styles.button} ${viewMode === 'board' ? styles.buttonSuccess : ''}`}
561
+ onClick={() => {
562
+ setViewMode('board');
563
+ syncUrlState({ view: 'board' });
564
+ }}
565
+ >
566
+ Board
567
+ </button>
568
+ <button
569
+ type="button"
570
+ className={`${styles.button} ${viewMode === 'list' ? styles.buttonSuccess : ''}`}
571
+ onClick={() => {
572
+ setViewMode('list');
573
+ syncUrlState({ view: 'list' });
574
+ }}
575
+ >
576
+ List
577
+ </button>
578
+ </div>
579
+
347
580
  <label className={styles.label} htmlFor="project-switcher">
348
581
  Project
349
582
  </label>
@@ -354,13 +587,7 @@ export default function Dashboard() {
354
587
  onChange={(event) => {
355
588
  const next = event.target.value;
356
589
  setProject(next);
357
- const url = new URL(window.location.href);
358
- if (next) {
359
- url.searchParams.set('project', next);
360
- } else {
361
- url.searchParams.delete('project');
362
- }
363
- window.history.replaceState(null, '', url.toString());
590
+ syncUrlState({ projectName: next });
364
591
  }}
365
592
  >
366
593
  <option value="">Current Repo</option>
@@ -381,27 +608,88 @@ export default function Dashboard() {
381
608
  counts={summaryCounts}
382
609
  activeQuickFilter={filters.quickFilter}
383
610
  onQuickFilter={setQuickFilter}
611
+ mergeHistogram14d={payload.metrics?.merge_histogram_14d}
612
+ totalCostTodayUsd={payload.metrics?.total_cost_today_usd}
613
+ analyticsHref={`/analytics${projectQuery(project)}`}
384
614
  />
385
615
 
386
616
  <FilterBar
387
617
  filters={filters}
388
- onFiltersChange={setFilters}
389
- onClearFilters={() => setFilters(createDefaultFilters())}
618
+ onFiltersChange={(next) => applyFilters(next, 'replace')}
619
+ onClearFilters={() => applyFilters(createDefaultFilters(), 'push')}
390
620
  searchInputRef={searchInputRef}
391
621
  />
392
622
 
393
623
  <div className={styles.layout}>
394
624
  <div>
395
- <KanbanBoard
396
- featuresByPhase={featuresByPhase}
397
- hasActiveFilters={hasActiveFilters}
398
- selectedFeatureId={selectedFeatureId}
399
- focusedFeatureId={focusedFeatureId}
400
- onSelectFeature={(featureId) => {
401
- setSelectedFeatureId(featureId);
402
- setFocusedFeatureId(featureId);
403
- }}
404
- onFocusFeature={setFocusedFeatureId}
625
+ <div className={styles.operatorGrid}>
626
+ <RunHealthPanel runtime={payload.runtime} nowMs={nowMs} />
627
+ <LockResourceMap
628
+ locks={payload.lock_map ?? []}
629
+ nowMs={nowMs}
630
+ onSelectFeature={(featureId) => {
631
+ setSelectedFeatureId(featureId);
632
+ setFocusedFeatureId(featureId);
633
+ syncUrlState({ featureId });
634
+ }}
635
+ />
636
+ <DependencyChains
637
+ chains={dependencyChains}
638
+ onSelectFeature={(featureId) => {
639
+ setSelectedFeatureId(featureId);
640
+ setFocusedFeatureId(featureId);
641
+ syncUrlState({ featureId });
642
+ }}
643
+ />
644
+ <QuickLaunchPanel
645
+ visible={isQuickLaunchAffordanceVisible(quickLaunchAvailability)}
646
+ featureId={quickLaunchFeatureId}
647
+ goal={quickLaunchGoal}
648
+ submitting={quickLaunchSubmitting}
649
+ disabledReason={quickLaunchAvailability.reason}
650
+ onFeatureIdChange={setQuickLaunchFeatureId}
651
+ onGoalChange={setQuickLaunchGoal}
652
+ onRequestLaunch={requestQuickLaunch}
653
+ />
654
+ </div>
655
+
656
+ {viewMode === 'list' ? (
657
+ <FeatureListView
658
+ features={filteredFeatures}
659
+ project={project}
660
+ selectedFeatureId={selectedFeatureId}
661
+ focusedFeatureId={focusedFeatureId}
662
+ onSelectFeature={(featureId) => {
663
+ setSelectedFeatureId(featureId);
664
+ setFocusedFeatureId(featureId);
665
+ syncUrlState({ featureId });
666
+ }}
667
+ onFocusFeature={setFocusedFeatureId}
668
+ />
669
+ ) : (
670
+ <KanbanBoard
671
+ featuresByPhase={featuresByPhase}
672
+ hasActiveFilters={hasActiveFilters}
673
+ selectedFeatureId={selectedFeatureId}
674
+ focusedFeatureId={focusedFeatureId}
675
+ dependencyBlockedFeatureIds={dependencyBlockedIds}
676
+ onSelectFeature={(featureId) => {
677
+ setSelectedFeatureId(featureId);
678
+ setFocusedFeatureId(featureId);
679
+ syncUrlState({ featureId });
680
+ }}
681
+ onFocusFeature={setFocusedFeatureId}
682
+ />
683
+ )}
684
+ <LiveEventFeed
685
+ events={feedState.events}
686
+ paused={feedState.paused}
687
+ onTogglePause={() =>
688
+ setFeedState((current) => reduceLiveFeedState(current, { type: 'toggle_pause' }))
689
+ }
690
+ onClear={() =>
691
+ setFeedState((current) => reduceLiveFeedState(current, { type: 'clear' }))
692
+ }
405
693
  />
406
694
  <CollisionQueue queue={payload.index.blocked_queue ?? []} />
407
695
  </div>
@@ -409,9 +697,12 @@ export default function Dashboard() {
409
697
  <DetailPanel
410
698
  detail={detail}
411
699
  project={project}
700
+ unresolvedDependencies={selectedDependencies}
701
+ flakySuspects={payload.flaky_suspects ?? []}
412
702
  onClose={() => {
413
703
  setSelectedFeatureId(null);
414
704
  setDetail(null);
705
+ syncUrlState({ featureId: null });
415
706
  }}
416
707
  refreshDetail={refreshDetail}
417
708
  refreshStatus={refreshStatus}
@@ -419,6 +710,17 @@ export default function Dashboard() {
419
710
  />
420
711
  </div>
421
712
 
713
+ <ConfirmationModal
714
+ isOpen={quickLaunchConfirmOpen}
715
+ title={`Launch ${quickLaunchFeatureId.trim()}?`}
716
+ message={`Goal:\n${quickLaunchGoal.trim()}\n\nThis action starts a new orchestrator feature run.`}
717
+ confirmLabel="Confirm Launch"
718
+ onCancel={() => setQuickLaunchConfirmOpen(false)}
719
+ onConfirm={() => {
720
+ void confirmQuickLaunch();
721
+ }}
722
+ />
723
+
422
724
  <ToastStack notifications={notifications} onDismiss={dismissToast} />
423
725
  </div>
424
726
  );