agentic-orchestrator 0.1.26 → 0.1.28
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/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +47 -14
- package/agentic/orchestrator/agents.yaml +13 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +76 -0
- package/agentic/orchestrator/schemas/policy.schema.json +16 -0
- package/agentic/orchestrator/schemas/policy.user.schema.json +16 -0
- package/agentic/orchestrator/schemas/state.schema.json +53 -0
- package/apps/control-plane/src/application/configuration-service.ts +181 -0
- package/apps/control-plane/src/application/kernel-tool-wiring.ts +292 -0
- package/apps/control-plane/src/application/services/checkpoint-service.ts +523 -0
- package/apps/control-plane/src/application/services/feature-send-message-service.ts +132 -0
- package/apps/control-plane/src/application/services/patch-service.ts +29 -5
- package/apps/control-plane/src/application/services/repo-operations-service.ts +276 -0
- package/apps/control-plane/src/application/services/worktree-watchdog-service.ts +156 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +12 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +17 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +31 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -4
- package/apps/control-plane/src/cli/rollback-command-handler.ts +217 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/types.ts +3 -0
- package/apps/control-plane/src/core/kernel-types.ts +55 -0
- package/apps/control-plane/src/core/kernel.ts +61 -878
- package/apps/control-plane/src/core/tool-caller.ts +10 -0
- package/apps/control-plane/src/core/utils/field-readers.ts +38 -0
- package/apps/control-plane/src/core/utils/index-normalizer.ts +119 -0
- package/apps/control-plane/src/core/utils/path-normalizers.ts +22 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +15 -0
- package/apps/control-plane/src/providers/api-worker-provider.ts +14 -12
- package/apps/control-plane/src/providers/cli-worker-provider.ts +82 -12
- package/apps/control-plane/src/providers/providers.ts +45 -24
- package/apps/control-plane/src/providers/worker-provider-factory.ts +36 -1
- package/apps/control-plane/src/supervisor/run-coordinator.ts +91 -36
- package/apps/control-plane/src/supervisor/runtime.ts +107 -1
- package/apps/control-plane/src/supervisor/types.ts +9 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +253 -14
- package/apps/control-plane/test/checkpoint-service.spec.ts +537 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +28 -0
- package/apps/control-plane/test/cli.unit.spec.ts +52 -0
- package/apps/control-plane/test/configuration-service.spec.ts +466 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +537 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +233 -0
- package/apps/control-plane/test/feature-send-message-service.spec.ts +314 -0
- package/apps/control-plane/test/init-wizard.spec.ts +35 -0
- package/apps/control-plane/test/path-normalizers.spec.ts +41 -0
- package/apps/control-plane/test/repo-operations-service.spec.ts +339 -0
- package/apps/control-plane/test/resume-command.spec.ts +33 -0
- package/apps/control-plane/test/review-workspace-logic.spec.ts +130 -0
- package/apps/control-plane/test/rollback-command.spec.ts +208 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +119 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +209 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +102 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +14 -0
- package/apps/control-plane/test/worktree-watchdog-service.spec.ts +147 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/dist/apps/control-plane/application/configuration-service.d.ts +19 -0
- package/dist/apps/control-plane/application/configuration-service.js +123 -0
- package/dist/apps/control-plane/application/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.d.ts +39 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js +38 -0
- package/dist/apps/control-plane/application/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.d.ts +84 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js +367 -0
- package/dist/apps/control-plane/application/services/checkpoint-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.d.ts +25 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js +105 -0
- package/dist/apps/control-plane/application/services/feature-send-message-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/patch-service.d.ts +6 -0
- package/dist/apps/control-plane/application/services/patch-service.js +11 -2
- package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/repo-operations-service.d.ts +70 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js +213 -0
- package/dist/apps/control-plane/application/services/repo-operations-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.d.ts +23 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js +119 -0
- package/dist/apps/control-plane/application/services/worktree-watchdog-service.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +12 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +17 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +23 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js +25 -5
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/rollback-command-handler.d.ts +6 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js +177 -0
- package/dist/apps/control-plane/cli/rollback-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +7 -1
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +3 -0
- package/dist/apps/control-plane/cli/types.js +1 -0
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/configuration-service.d.ts +25 -0
- package/dist/apps/control-plane/core/configuration-service.js +130 -0
- package/dist/apps/control-plane/core/configuration-service.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.d.ts +50 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js +44 -0
- package/dist/apps/control-plane/core/kernel-tool-wiring.js.map +1 -0
- package/dist/apps/control-plane/core/kernel-types.d.ts +48 -0
- package/dist/apps/control-plane/core/kernel-types.js +2 -0
- package/dist/apps/control-plane/core/kernel-types.js.map +1 -0
- package/dist/apps/control-plane/core/kernel.d.ts +17 -48
- package/dist/apps/control-plane/core/kernel.js +44 -539
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +10 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js +51 -0
- package/dist/apps/control-plane/core/utils/error-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/field-readers.d.ts +9 -0
- package/dist/apps/control-plane/core/utils/field-readers.js +30 -0
- package/dist/apps/control-plane/core/utils/field-readers.js.map +1 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.d.ts +7 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js +92 -0
- package/dist/apps/control-plane/core/utils/index-normalizer.js.map +1 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.d.ts +2 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js +17 -0
- package/dist/apps/control-plane/core/utils/path-normalizers.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +13 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +4 -13
- package/dist/apps/control-plane/providers/api-worker-provider.js +10 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +11 -13
- package/dist/apps/control-plane/providers/cli-worker-provider.js +64 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +31 -24
- package/dist/apps/control-plane/providers/providers.js +10 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +11 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +20 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +81 -33
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +8 -1
- package/dist/apps/control-plane/supervisor/runtime.js +90 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +11 -0
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +21 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +207 -13
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/package.json +2 -0
- package/packages/web-dashboard/src/app/analytics/page.tsx +83 -2
- package/packages/web-dashboard/src/app/api/actions/route.ts +92 -1
- package/packages/web-dashboard/src/app/api/analytics/route.ts +5 -2
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/[checkpointId]/diff/route.ts +43 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/compare/route.ts +45 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkpoints/stream/route.ts +170 -0
- package/packages/web-dashboard/src/app/api/features/[id]/file-diff/route.ts +144 -0
- package/packages/web-dashboard/src/app/api/features/[id]/log-stream/route.ts +167 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/[filename]/route.ts +65 -0
- package/packages/web-dashboard/src/app/api/features/[id]/raw-logs/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/timeline/route.ts +60 -0
- package/packages/web-dashboard/src/app/feature/[id]/page.tsx +32 -11
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/components/detail-panel.tsx +483 -0
- package/packages/web-dashboard/src/components/review-workspace.tsx +1162 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +725 -0
- package/packages/web-dashboard/src/lib/review-contracts.ts +182 -0
- package/packages/web-dashboard/src/lib/review-workspace-logic.ts +64 -0
- package/packages/web-dashboard/src/lib/types.ts +131 -0
- package/packages/web-dashboard/src/styles/dashboard.module.css +333 -0
- package/spec-files/completed/agentic_orchestrator_execution_mode_spec.md +1905 -0
- package/spec-files/outstanding/agentic_orchestrator_runtime_inspection_spec.md +940 -0
- package/spec-files/outstanding/execution_mode_critical_review.md +355 -0
- package/spec-files/outstanding/shadow_workspace_implementation_spec.md +1271 -0
- package/spec-files/outstanding/shadow_workspace_spec_summary.md +222 -0
- package/spec-files/progress.md +269 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFeatureState,
|
|
3
|
+
readWorkerTimelineEntries,
|
|
4
|
+
resolveProjectRoot,
|
|
5
|
+
} from '@/lib/aop-client.js';
|
|
6
|
+
import { errorEnvelope, okEnvelope } from '@/lib/api-envelope.js';
|
|
7
|
+
import { FEATURE_ID_PATTERN, normalizeLimit } from '@/lib/review-contracts.js';
|
|
8
|
+
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
11
|
+
export async function GET(
|
|
12
|
+
req: Request,
|
|
13
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
14
|
+
): Promise<Response> {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
if (!FEATURE_ID_PATTERN.test(id)) {
|
|
17
|
+
return Response.json(errorEnvelope('invalid_input', 'Invalid feature id'), { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = new URL(req.url);
|
|
21
|
+
const project = url.searchParams.get('project');
|
|
22
|
+
const role = url.searchParams.get('role');
|
|
23
|
+
const validityRaw = url.searchParams.get('valid');
|
|
24
|
+
const validity = validityRaw === 'valid' || validityRaw === 'invalid' ? validityRaw : 'all';
|
|
25
|
+
const limit = normalizeLimit(url.searchParams.get('limit'), 100, 100);
|
|
26
|
+
const repoRoot = await resolveProjectRoot(project);
|
|
27
|
+
|
|
28
|
+
const state = await readFeatureState(id, repoRoot);
|
|
29
|
+
if (!state) {
|
|
30
|
+
return Response.json(errorEnvelope('feature_not_found', `Feature ${id} not found`), {
|
|
31
|
+
status: 404,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entries = await readWorkerTimelineEntries(id, repoRoot, {
|
|
36
|
+
role,
|
|
37
|
+
valid: validity,
|
|
38
|
+
limit,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const summary = {
|
|
42
|
+
total: entries.length,
|
|
43
|
+
valid: entries.filter((entry) => entry.valid).length,
|
|
44
|
+
invalid: entries.filter((entry) => !entry.valid).length,
|
|
45
|
+
roles: entries.reduce<Record<string, number>>((acc, entry) => {
|
|
46
|
+
acc[entry.role] = (acc[entry.role] ?? 0) + 1;
|
|
47
|
+
return acc;
|
|
48
|
+
}, {}),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return Response.json(
|
|
52
|
+
okEnvelope(
|
|
53
|
+
{
|
|
54
|
+
entries,
|
|
55
|
+
summary,
|
|
56
|
+
},
|
|
57
|
+
{ source: 'artifact' },
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { DetailPanel } from '@/components/detail-panel';
|
|
6
|
+
import { ReviewWorkspace } from '@/components/review-workspace';
|
|
6
7
|
import { ToastStack, useToast } from '@/components/toast';
|
|
7
8
|
import type { DashboardStatusPayload, FeatureDetail } from '@/lib/types.js';
|
|
8
9
|
import styles from '@/styles/dashboard.module.css';
|
|
@@ -14,6 +15,10 @@ function emptyPayload(): DashboardStatusPayload {
|
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function hasActiveRuntimeRunId(runId: string | null | undefined): boolean {
|
|
19
|
+
return Boolean(runId) && runId !== 'unknown' && runId !== 'unassigned' && runId !== 'none';
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
async function jsonFetch<T>(input: string, init?: RequestInit): Promise<T> {
|
|
18
23
|
const response = await fetch(input, init);
|
|
19
24
|
return (await response.json()) as T;
|
|
@@ -77,6 +82,11 @@ export default function FeatureFocusPage({ params }: { params: Promise<{ id: str
|
|
|
77
82
|
payload.features.find((feature) => feature.feature_id === featureId)?.status ??
|
|
78
83
|
detail?.feature.status ??
|
|
79
84
|
'unknown';
|
|
85
|
+
const showReviewWorkspace =
|
|
86
|
+
detail?.feature.phase === 'ready_to_merge' || detail?.feature.phase === 'qa';
|
|
87
|
+
const hasActiveRuntimeSession =
|
|
88
|
+
hasActiveRuntimeRunId(payload.runtime?.run_id) ||
|
|
89
|
+
hasActiveRuntimeRunId(payload.index.runtime_sessions?.run_id);
|
|
80
90
|
|
|
81
91
|
return (
|
|
82
92
|
<div className={styles.dashboardRoot}>
|
|
@@ -93,17 +103,28 @@ export default function FeatureFocusPage({ params }: { params: Promise<{ id: str
|
|
|
93
103
|
</header>
|
|
94
104
|
|
|
95
105
|
<div className={styles.focusLayout}>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
{detail && showReviewWorkspace ? (
|
|
107
|
+
<ReviewWorkspace
|
|
108
|
+
detail={detail}
|
|
109
|
+
project={project}
|
|
110
|
+
hasActiveRuntimeSession={hasActiveRuntimeSession}
|
|
111
|
+
refreshDetail={refreshDetail}
|
|
112
|
+
refreshStatus={refreshStatus}
|
|
113
|
+
addToast={addToast}
|
|
114
|
+
/>
|
|
115
|
+
) : (
|
|
116
|
+
<DetailPanel
|
|
117
|
+
detail={detail}
|
|
118
|
+
project={project}
|
|
119
|
+
isFocusView
|
|
120
|
+
onClose={() => {
|
|
121
|
+
window.location.href = `/${query.length > 0 ? query : ''}`;
|
|
122
|
+
}}
|
|
123
|
+
refreshDetail={refreshDetail}
|
|
124
|
+
refreshStatus={refreshStatus}
|
|
125
|
+
addToast={addToast}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
107
128
|
</div>
|
|
108
129
|
|
|
109
130
|
<ToastStack notifications={notifications} onDismiss={dismissToast} />
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from '@/lib/dashboard-utils.js';
|
|
7
7
|
import type {
|
|
8
8
|
CostSummary,
|
|
9
|
+
FeatureCheckpointComparison,
|
|
10
|
+
FeatureCheckpoint,
|
|
9
11
|
FeatureDetail,
|
|
10
12
|
FeatureLog,
|
|
11
13
|
FeatureLogEntry,
|
|
@@ -80,6 +82,14 @@ interface ApiEnvelope<TData> {
|
|
|
80
82
|
error?: { code?: string; message?: string; retryable?: boolean };
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
interface CheckpointStreamPayload {
|
|
86
|
+
type: 'snapshot' | 'append' | 'heartbeat' | 'error';
|
|
87
|
+
cursor: string;
|
|
88
|
+
execution_mode: 'deterministic' | 'interactive' | null;
|
|
89
|
+
checkpoints: FeatureCheckpoint[];
|
|
90
|
+
message?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
async function callAction(
|
|
84
94
|
action: ReviewAction,
|
|
85
95
|
featureId: string,
|
|
@@ -94,6 +104,59 @@ async function callAction(
|
|
|
94
104
|
return (await response.json()) as ActionResponse;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
function formatCheckpointTimestamp(value: string): string {
|
|
108
|
+
const parsed = Date.parse(value);
|
|
109
|
+
if (Number.isNaN(parsed)) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
return new Date(parsed).toLocaleString([], {
|
|
113
|
+
month: 'short',
|
|
114
|
+
day: '2-digit',
|
|
115
|
+
hour: '2-digit',
|
|
116
|
+
minute: '2-digit',
|
|
117
|
+
second: '2-digit',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkpointStatusBadge(status: FeatureCheckpoint['validation_status']): string {
|
|
122
|
+
if (status === 'valid') {
|
|
123
|
+
return `${styles.badge} ${styles.badgePass}`;
|
|
124
|
+
}
|
|
125
|
+
if (status === 'invalid') {
|
|
126
|
+
return `${styles.badge} ${styles.badgeFail}`;
|
|
127
|
+
}
|
|
128
|
+
return `${styles.badge} ${styles.badgePending}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function checkpointSeverityBadge(severity: FeatureCheckpoint['severity']): string {
|
|
132
|
+
if (severity === 'error' || severity === 'critical') {
|
|
133
|
+
return `${styles.badge} ${styles.badgeFail}`;
|
|
134
|
+
}
|
|
135
|
+
if (severity === 'warning') {
|
|
136
|
+
return `${styles.badge} ${styles.badgePending}`;
|
|
137
|
+
}
|
|
138
|
+
if (severity === 'info') {
|
|
139
|
+
return `${styles.badge} ${styles.badgeNeutral}`;
|
|
140
|
+
}
|
|
141
|
+
return `${styles.badge} ${styles.badgeNeutral}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mergeCheckpointTimeline(
|
|
145
|
+
current: FeatureCheckpoint[],
|
|
146
|
+
incoming: FeatureCheckpoint[],
|
|
147
|
+
): FeatureCheckpoint[] {
|
|
148
|
+
const byId = new Map<string, FeatureCheckpoint>();
|
|
149
|
+
for (const item of current) {
|
|
150
|
+
byId.set(item.checkpoint_id, item);
|
|
151
|
+
}
|
|
152
|
+
for (const item of incoming) {
|
|
153
|
+
byId.set(item.checkpoint_id, item);
|
|
154
|
+
}
|
|
155
|
+
return [...byId.values()].sort(
|
|
156
|
+
(left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
export function DetailPanel({
|
|
98
161
|
detail,
|
|
99
162
|
project,
|
|
@@ -117,6 +180,25 @@ export function DetailPanel({
|
|
|
117
180
|
const [cost, setCost] = useState<CostSummary | null>(detail?.cost ?? null);
|
|
118
181
|
const [qaIndex, setQaIndex] = useState<QaTestIndex | null>(detail?.qa_test_index ?? null);
|
|
119
182
|
const [reviewBrief, setReviewBrief] = useState<ReviewBrief | null>(detail?.review_brief ?? null);
|
|
183
|
+
const [runtimeInspectorView, setRuntimeInspectorView] = useState<
|
|
184
|
+
'timeline' | 'snapshot' | 'compare'
|
|
185
|
+
>('timeline');
|
|
186
|
+
const [checkpointTimeline, setCheckpointTimeline] = useState<FeatureCheckpoint[]>(
|
|
187
|
+
detail?.checkpoints ?? [],
|
|
188
|
+
);
|
|
189
|
+
const [checkpointStreamError, setCheckpointStreamError] = useState<string | null>(null);
|
|
190
|
+
const [selectedCheckpointId, setSelectedCheckpointId] = useState<string | null>(null);
|
|
191
|
+
const [selectedCheckpointDiff, setSelectedCheckpointDiff] = useState<string>('');
|
|
192
|
+
const [selectedCheckpointDiffLoading, setSelectedCheckpointDiffLoading] = useState(false);
|
|
193
|
+
const [selectedCheckpointDiffError, setSelectedCheckpointDiffError] = useState<string | null>(
|
|
194
|
+
null,
|
|
195
|
+
);
|
|
196
|
+
const [compareFromCheckpointId, setCompareFromCheckpointId] = useState<string>('');
|
|
197
|
+
const [compareToCheckpointId, setCompareToCheckpointId] = useState<string>('');
|
|
198
|
+
const [checkpointComparison, setCheckpointComparison] =
|
|
199
|
+
useState<FeatureCheckpointComparison | null>(null);
|
|
200
|
+
const [checkpointComparisonLoading, setCheckpointComparisonLoading] = useState(false);
|
|
201
|
+
const [checkpointComparisonError, setCheckpointComparisonError] = useState<string | null>(null);
|
|
120
202
|
const [budgetLimitUsd, setBudgetLimitUsd] = useState<number | null>(null);
|
|
121
203
|
const costCacheRef = useRef<Map<string, { expiresAt: number; value: CostSummary | null }>>(
|
|
122
204
|
new Map(),
|
|
@@ -301,12 +383,217 @@ export function DetailPanel({
|
|
|
301
383
|
};
|
|
302
384
|
}, [detail, project]);
|
|
303
385
|
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
setCheckpointTimeline(detail?.checkpoints ?? []);
|
|
388
|
+
setCheckpointStreamError(null);
|
|
389
|
+
}, [detail?.feature.feature_id, detail?.checkpoints]);
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (checkpointTimeline.length === 0) {
|
|
393
|
+
setSelectedCheckpointId(null);
|
|
394
|
+
setCompareFromCheckpointId('');
|
|
395
|
+
setCompareToCheckpointId('');
|
|
396
|
+
setCheckpointComparison(null);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
setSelectedCheckpointId((current) => {
|
|
401
|
+
if (
|
|
402
|
+
current &&
|
|
403
|
+
checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
|
|
404
|
+
) {
|
|
405
|
+
return current;
|
|
406
|
+
}
|
|
407
|
+
return checkpointTimeline[0].checkpoint_id;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
setCompareFromCheckpointId((current) => {
|
|
411
|
+
if (
|
|
412
|
+
current &&
|
|
413
|
+
checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
|
|
414
|
+
) {
|
|
415
|
+
return current;
|
|
416
|
+
}
|
|
417
|
+
return checkpointTimeline[0].checkpoint_id;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
setCompareToCheckpointId((current) => {
|
|
421
|
+
if (
|
|
422
|
+
current &&
|
|
423
|
+
checkpointTimeline.some((checkpoint) => checkpoint.checkpoint_id === current)
|
|
424
|
+
) {
|
|
425
|
+
return current;
|
|
426
|
+
}
|
|
427
|
+
return checkpointTimeline[1]?.checkpoint_id ?? '';
|
|
428
|
+
});
|
|
429
|
+
}, [checkpointTimeline]);
|
|
430
|
+
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
if (!detail) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const shouldStream =
|
|
436
|
+
detail.execution_mode === 'interactive' || (detail.checkpoints?.length ?? 0) > 0;
|
|
437
|
+
if (!shouldStream) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const params = new URLSearchParams();
|
|
441
|
+
if (project.length > 0) {
|
|
442
|
+
params.set('project', project);
|
|
443
|
+
}
|
|
444
|
+
const eventSource = new EventSource(
|
|
445
|
+
`/api/features/${encodeURIComponent(detail.feature.feature_id)}/checkpoints/stream?${params.toString()}`,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const onStreamEvent = (event: MessageEvent<string>) => {
|
|
449
|
+
let payload: CheckpointStreamPayload;
|
|
450
|
+
try {
|
|
451
|
+
payload = JSON.parse(event.data) as CheckpointStreamPayload;
|
|
452
|
+
} catch {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (payload.type === 'error') {
|
|
456
|
+
setCheckpointStreamError(payload.message ?? 'Checkpoint stream warning');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (payload.checkpoints.length === 0) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
setCheckpointStreamError(null);
|
|
463
|
+
if (payload.type === 'snapshot') {
|
|
464
|
+
setCheckpointTimeline(payload.checkpoints);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (payload.type === 'append') {
|
|
468
|
+
setCheckpointTimeline((current) => mergeCheckpointTimeline(current, payload.checkpoints));
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
eventSource.addEventListener('snapshot', onStreamEvent as EventListener);
|
|
473
|
+
eventSource.addEventListener('append', onStreamEvent as EventListener);
|
|
474
|
+
eventSource.addEventListener('error', () => {
|
|
475
|
+
setCheckpointStreamError('Disconnected from checkpoint stream');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return () => {
|
|
479
|
+
eventSource.close();
|
|
480
|
+
};
|
|
481
|
+
}, [detail?.feature.feature_id, detail?.execution_mode, detail?.checkpoints?.length, project]);
|
|
482
|
+
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (!detail || !selectedCheckpointId) {
|
|
485
|
+
setSelectedCheckpointDiff('');
|
|
486
|
+
setSelectedCheckpointDiffError(null);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const controller = new AbortController();
|
|
490
|
+
let cancelled = false;
|
|
491
|
+
|
|
492
|
+
void (async () => {
|
|
493
|
+
setSelectedCheckpointDiffLoading(true);
|
|
494
|
+
setSelectedCheckpointDiffError(null);
|
|
495
|
+
try {
|
|
496
|
+
const response = await fetch(
|
|
497
|
+
`/api/features/${encodeURIComponent(
|
|
498
|
+
detail.feature.feature_id,
|
|
499
|
+
)}/checkpoints/${encodeURIComponent(selectedCheckpointId)}/diff${projectQuery(project)}`,
|
|
500
|
+
{ signal: controller.signal },
|
|
501
|
+
);
|
|
502
|
+
const body = (await response.json()) as ApiEnvelope<{
|
|
503
|
+
checkpoint: FeatureCheckpoint;
|
|
504
|
+
diff: string;
|
|
505
|
+
}>;
|
|
506
|
+
if (cancelled) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!body.ok || !body.data) {
|
|
510
|
+
setSelectedCheckpointDiff('');
|
|
511
|
+
setSelectedCheckpointDiffError(body.error?.message ?? 'Unable to load checkpoint diff');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
setSelectedCheckpointDiff(body.data.diff);
|
|
515
|
+
} catch {
|
|
516
|
+
if (!cancelled) {
|
|
517
|
+
setSelectedCheckpointDiff('');
|
|
518
|
+
setSelectedCheckpointDiffError('Unable to load checkpoint diff');
|
|
519
|
+
}
|
|
520
|
+
} finally {
|
|
521
|
+
if (!cancelled) {
|
|
522
|
+
setSelectedCheckpointDiffLoading(false);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
})();
|
|
526
|
+
|
|
527
|
+
return () => {
|
|
528
|
+
cancelled = true;
|
|
529
|
+
controller.abort();
|
|
530
|
+
};
|
|
531
|
+
}, [detail, selectedCheckpointId, project]);
|
|
532
|
+
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
if (!detail || !compareFromCheckpointId || !compareToCheckpointId) {
|
|
535
|
+
setCheckpointComparison(null);
|
|
536
|
+
setCheckpointComparisonError(null);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (compareFromCheckpointId === compareToCheckpointId) {
|
|
540
|
+
setCheckpointComparison(null);
|
|
541
|
+
setCheckpointComparisonError('Select two different checkpoints to compare.');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const controller = new AbortController();
|
|
546
|
+
let cancelled = false;
|
|
547
|
+
void (async () => {
|
|
548
|
+
setCheckpointComparisonLoading(true);
|
|
549
|
+
setCheckpointComparisonError(null);
|
|
550
|
+
try {
|
|
551
|
+
const query = new URLSearchParams();
|
|
552
|
+
query.set('from', compareFromCheckpointId);
|
|
553
|
+
query.set('to', compareToCheckpointId);
|
|
554
|
+
if (project.length > 0) {
|
|
555
|
+
query.set('project', project);
|
|
556
|
+
}
|
|
557
|
+
const response = await fetch(
|
|
558
|
+
`/api/features/${encodeURIComponent(detail.feature.feature_id)}/checkpoints/compare?${query.toString()}`,
|
|
559
|
+
{ signal: controller.signal },
|
|
560
|
+
);
|
|
561
|
+
const body = (await response.json()) as ApiEnvelope<FeatureCheckpointComparison>;
|
|
562
|
+
if (cancelled) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!body.ok || !body.data) {
|
|
566
|
+
setCheckpointComparison(null);
|
|
567
|
+
setCheckpointComparisonError(body.error?.message ?? 'Unable to compare checkpoints');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
setCheckpointComparison(body.data);
|
|
571
|
+
} catch {
|
|
572
|
+
if (!cancelled) {
|
|
573
|
+
setCheckpointComparison(null);
|
|
574
|
+
setCheckpointComparisonError('Unable to compare checkpoints');
|
|
575
|
+
}
|
|
576
|
+
} finally {
|
|
577
|
+
if (!cancelled) {
|
|
578
|
+
setCheckpointComparisonLoading(false);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
})();
|
|
582
|
+
|
|
583
|
+
return () => {
|
|
584
|
+
cancelled = true;
|
|
585
|
+
controller.abort();
|
|
586
|
+
};
|
|
587
|
+
}, [detail, compareFromCheckpointId, compareToCheckpointId, project]);
|
|
588
|
+
|
|
304
589
|
const phase = detail?.feature.phase ?? 'unknown';
|
|
305
590
|
|
|
306
591
|
const shouldShowPlan = phase === 'planning' || phase === 'building';
|
|
307
592
|
const shouldShowDiff = ['building', 'qa', 'ready_to_merge', 'blocked'].includes(phase);
|
|
308
593
|
const shouldShowGates = ['qa', 'ready_to_merge', 'blocked'].includes(phase);
|
|
309
594
|
const shouldShowEvidence = ['qa', 'ready_to_merge', 'blocked'].includes(phase);
|
|
595
|
+
const checkpoints = checkpointTimeline;
|
|
596
|
+
const showCheckpointTimeline = detail?.execution_mode === 'interactive' || checkpoints.length > 0;
|
|
310
597
|
|
|
311
598
|
const timeInPhase = useMemo(
|
|
312
599
|
() => formatElapsedTime(detail?.feature.last_updated, nowMs),
|
|
@@ -466,6 +753,202 @@ export function DetailPanel({
|
|
|
466
753
|
|
|
467
754
|
{detail.feature.pr ? <PrStatusCard pr={detail.feature.pr} /> : null}
|
|
468
755
|
|
|
756
|
+
{showCheckpointTimeline ? (
|
|
757
|
+
<details className={styles.section} open={checkpoints.length > 0}>
|
|
758
|
+
<summary className={styles.sectionSummary}>Runtime Inspector · Checkpoints</summary>
|
|
759
|
+
<div className={styles.badgeRow}>
|
|
760
|
+
<span className={`${styles.badge} ${styles.badgeNeutral}`}>
|
|
761
|
+
mode: {detail.execution_mode ?? 'deterministic'}
|
|
762
|
+
</span>
|
|
763
|
+
<span className={`${styles.badge} ${styles.badgeNeutral}`}>
|
|
764
|
+
{checkpoints.length} checkpoints
|
|
765
|
+
</span>
|
|
766
|
+
{checkpointStreamError ? (
|
|
767
|
+
<span className={`${styles.badge} ${styles.badgeFail}`}>{checkpointStreamError}</span>
|
|
768
|
+
) : (
|
|
769
|
+
<span className={`${styles.badge} ${styles.badgePass}`}>streaming</span>
|
|
770
|
+
)}
|
|
771
|
+
</div>
|
|
772
|
+
<div className={styles.runtimeInspectorTabs}>
|
|
773
|
+
<button
|
|
774
|
+
type="button"
|
|
775
|
+
className={`${styles.runtimeInspectorTab} ${
|
|
776
|
+
runtimeInspectorView === 'timeline' ? styles.runtimeInspectorTabActive : ''
|
|
777
|
+
}`}
|
|
778
|
+
onClick={() => setRuntimeInspectorView('timeline')}
|
|
779
|
+
>
|
|
780
|
+
Timeline
|
|
781
|
+
</button>
|
|
782
|
+
<button
|
|
783
|
+
type="button"
|
|
784
|
+
className={`${styles.runtimeInspectorTab} ${
|
|
785
|
+
runtimeInspectorView === 'snapshot' ? styles.runtimeInspectorTabActive : ''
|
|
786
|
+
}`}
|
|
787
|
+
onClick={() => setRuntimeInspectorView('snapshot')}
|
|
788
|
+
disabled={checkpoints.length === 0}
|
|
789
|
+
>
|
|
790
|
+
Snapshot Diff
|
|
791
|
+
</button>
|
|
792
|
+
<button
|
|
793
|
+
type="button"
|
|
794
|
+
className={`${styles.runtimeInspectorTab} ${
|
|
795
|
+
runtimeInspectorView === 'compare' ? styles.runtimeInspectorTabActive : ''
|
|
796
|
+
}`}
|
|
797
|
+
onClick={() => setRuntimeInspectorView('compare')}
|
|
798
|
+
disabled={checkpoints.length < 2}
|
|
799
|
+
>
|
|
800
|
+
Compare
|
|
801
|
+
</button>
|
|
802
|
+
</div>
|
|
803
|
+
{runtimeInspectorView === 'timeline' ? (
|
|
804
|
+
checkpoints.length === 0 ? (
|
|
805
|
+
<p className={styles.metaText}>No checkpoints recorded for this feature yet.</p>
|
|
806
|
+
) : (
|
|
807
|
+
<div className={styles.reviewTimelineList}>
|
|
808
|
+
{checkpoints.map((checkpoint) => (
|
|
809
|
+
<div
|
|
810
|
+
key={`${checkpoint.checkpoint_id}:${checkpoint.timestamp}`}
|
|
811
|
+
className={styles.reviewTimelineEntry}
|
|
812
|
+
>
|
|
813
|
+
<div className={styles.badgeRow}>
|
|
814
|
+
<span className={checkpointStatusBadge(checkpoint.validation_status)}>
|
|
815
|
+
{checkpoint.validation_status}
|
|
816
|
+
</span>
|
|
817
|
+
{checkpoint.severity ? (
|
|
818
|
+
<span className={checkpointSeverityBadge(checkpoint.severity)}>
|
|
819
|
+
{checkpoint.severity}
|
|
820
|
+
</span>
|
|
821
|
+
) : null}
|
|
822
|
+
</div>
|
|
823
|
+
<span>{formatCheckpointTimestamp(checkpoint.timestamp)}</span>
|
|
824
|
+
<span>{checkpoint.files_changed.length} files</span>
|
|
825
|
+
<span>{checkpoint.checkpoint_id}</span>
|
|
826
|
+
{checkpoint.violations.length > 0 ? (
|
|
827
|
+
<span>{checkpoint.violations[0]}</span>
|
|
828
|
+
) : (
|
|
829
|
+
<span>no violations</span>
|
|
830
|
+
)}
|
|
831
|
+
</div>
|
|
832
|
+
))}
|
|
833
|
+
</div>
|
|
834
|
+
)
|
|
835
|
+
) : null}
|
|
836
|
+
{runtimeInspectorView === 'snapshot' ? (
|
|
837
|
+
<div className={styles.runtimeInspectorPanel}>
|
|
838
|
+
<label className={styles.label} htmlFor="checkpoint-select">
|
|
839
|
+
Checkpoint
|
|
840
|
+
</label>
|
|
841
|
+
<select
|
|
842
|
+
id="checkpoint-select"
|
|
843
|
+
className={styles.actionSelect}
|
|
844
|
+
value={selectedCheckpointId ?? ''}
|
|
845
|
+
onChange={(event) => setSelectedCheckpointId(event.target.value)}
|
|
846
|
+
>
|
|
847
|
+
{checkpoints.map((checkpoint) => (
|
|
848
|
+
<option key={checkpoint.checkpoint_id} value={checkpoint.checkpoint_id}>
|
|
849
|
+
{checkpoint.checkpoint_id} · {formatCheckpointTimestamp(checkpoint.timestamp)}
|
|
850
|
+
</option>
|
|
851
|
+
))}
|
|
852
|
+
</select>
|
|
853
|
+
{selectedCheckpointDiffLoading ? (
|
|
854
|
+
<p className={styles.metaText}>Loading checkpoint diff...</p>
|
|
855
|
+
) : null}
|
|
856
|
+
{selectedCheckpointDiffError ? (
|
|
857
|
+
<p className={styles.metaText}>{selectedCheckpointDiffError}</p>
|
|
858
|
+
) : null}
|
|
859
|
+
{!selectedCheckpointDiffLoading && !selectedCheckpointDiffError ? (
|
|
860
|
+
<DiffViewer diff={selectedCheckpointDiff} />
|
|
861
|
+
) : null}
|
|
862
|
+
</div>
|
|
863
|
+
) : null}
|
|
864
|
+
{runtimeInspectorView === 'compare' ? (
|
|
865
|
+
<div className={styles.runtimeInspectorPanel}>
|
|
866
|
+
<div className={styles.runtimeInspectorCompareGrid}>
|
|
867
|
+
<div>
|
|
868
|
+
<label className={styles.label} htmlFor="compare-from-checkpoint">
|
|
869
|
+
From checkpoint
|
|
870
|
+
</label>
|
|
871
|
+
<select
|
|
872
|
+
id="compare-from-checkpoint"
|
|
873
|
+
className={styles.actionSelect}
|
|
874
|
+
value={compareFromCheckpointId}
|
|
875
|
+
onChange={(event) => setCompareFromCheckpointId(event.target.value)}
|
|
876
|
+
>
|
|
877
|
+
{checkpoints.map((checkpoint) => (
|
|
878
|
+
<option
|
|
879
|
+
key={`from-${checkpoint.checkpoint_id}`}
|
|
880
|
+
value={checkpoint.checkpoint_id}
|
|
881
|
+
>
|
|
882
|
+
{checkpoint.checkpoint_id}
|
|
883
|
+
</option>
|
|
884
|
+
))}
|
|
885
|
+
</select>
|
|
886
|
+
</div>
|
|
887
|
+
<div>
|
|
888
|
+
<label className={styles.label} htmlFor="compare-to-checkpoint">
|
|
889
|
+
To checkpoint
|
|
890
|
+
</label>
|
|
891
|
+
<select
|
|
892
|
+
id="compare-to-checkpoint"
|
|
893
|
+
className={styles.actionSelect}
|
|
894
|
+
value={compareToCheckpointId}
|
|
895
|
+
onChange={(event) => setCompareToCheckpointId(event.target.value)}
|
|
896
|
+
>
|
|
897
|
+
{checkpoints.map((checkpoint) => (
|
|
898
|
+
<option
|
|
899
|
+
key={`to-${checkpoint.checkpoint_id}`}
|
|
900
|
+
value={checkpoint.checkpoint_id}
|
|
901
|
+
>
|
|
902
|
+
{checkpoint.checkpoint_id}
|
|
903
|
+
</option>
|
|
904
|
+
))}
|
|
905
|
+
</select>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
{checkpointComparisonLoading ? (
|
|
909
|
+
<p className={styles.metaText}>Comparing checkpoints...</p>
|
|
910
|
+
) : null}
|
|
911
|
+
{checkpointComparisonError ? (
|
|
912
|
+
<p className={styles.metaText}>{checkpointComparisonError}</p>
|
|
913
|
+
) : null}
|
|
914
|
+
{checkpointComparison ? (
|
|
915
|
+
<>
|
|
916
|
+
<div className={styles.badgeRow}>
|
|
917
|
+
<span className={`${styles.badge} ${styles.badgePass}`}>
|
|
918
|
+
added {checkpointComparison.summary.added}
|
|
919
|
+
</span>
|
|
920
|
+
<span className={`${styles.badge} ${styles.badgePending}`}>
|
|
921
|
+
changed {checkpointComparison.summary.changed}
|
|
922
|
+
</span>
|
|
923
|
+
<span className={`${styles.badge} ${styles.badgeFail}`}>
|
|
924
|
+
removed {checkpointComparison.summary.removed}
|
|
925
|
+
</span>
|
|
926
|
+
<span className={`${styles.badge} ${styles.badgeNeutral}`}>
|
|
927
|
+
unchanged {checkpointComparison.summary.unchanged}
|
|
928
|
+
</span>
|
|
929
|
+
</div>
|
|
930
|
+
<p className={styles.metaText}>
|
|
931
|
+
{checkpointComparison.files.length} files compared between{' '}
|
|
932
|
+
{checkpointComparison.from_checkpoint_id} and{' '}
|
|
933
|
+
{checkpointComparison.to_checkpoint_id}.
|
|
934
|
+
</p>
|
|
935
|
+
<div className={styles.runtimeInspectorCompareGrid}>
|
|
936
|
+
<div>
|
|
937
|
+
<h4 className={styles.sectionTitle}>From Snapshot</h4>
|
|
938
|
+
<DiffViewer diff={checkpointComparison.from_diff} />
|
|
939
|
+
</div>
|
|
940
|
+
<div>
|
|
941
|
+
<h4 className={styles.sectionTitle}>To Snapshot</h4>
|
|
942
|
+
<DiffViewer diff={checkpointComparison.to_diff} />
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
</>
|
|
946
|
+
) : null}
|
|
947
|
+
</div>
|
|
948
|
+
) : null}
|
|
949
|
+
</details>
|
|
950
|
+
) : null}
|
|
951
|
+
|
|
469
952
|
{shouldShowPlan ? <PlanViewer plan={detail.plan} /> : null}
|
|
470
953
|
{shouldShowGates ? <GateResults feature={detail.feature} /> : null}
|
|
471
954
|
{shouldShowGates ? <GateStepDrilldown evidence={detail.gate_evidence} /> : null}
|