@tonyclaw/agent-inspector 4.0.1 → 4.0.3
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/.output/backend/nitro.json +1 -1
- package/.output/cli.js +193 -110
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-CTHqmroT.mjs → router-ChILzhmz.mjs} +92 -4
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-Co3cUF_I.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-jFcrL37X.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-BPzt_VpM.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-C6O_zuwI.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-Ca6NvB7L.js} +1 -1
- package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BzqKcveJ.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-9pCZIsS_.js} +1 -1
- package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-BMP3KVGR.js} +1 -1
- package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-y4eRoT1H.js} +1 -1
- package/.output/ui/assets/{index-Ea-c8hPg.js → index-CH5w-haG.js} +1 -1
- package/.output/ui/assets/{index-zVVS53Hd.js → index-CmocEhyU.js} +1 -1
- package/.output/ui/assets/{index-Du4qNOOt.js → index-DT8sMJ_n.js} +2 -2
- package/.output/ui/assets/{index-gvFj_9K6.js → index-bfsbIRZa.js} +1 -1
- package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-BfFOSDG5.js} +1 -1
- package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkbVkOsp.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +5 -10
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1433
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -258
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
|
@@ -1,1304 +0,0 @@
|
|
|
1
|
-
import { Fragment, type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Check,
|
|
4
|
-
ChevronDown,
|
|
5
|
-
ChevronRight,
|
|
6
|
-
Copy,
|
|
7
|
-
Download,
|
|
8
|
-
ExternalLink,
|
|
9
|
-
Layers,
|
|
10
|
-
Loader2,
|
|
11
|
-
RefreshCw,
|
|
12
|
-
Trash2,
|
|
13
|
-
} from "lucide-react";
|
|
14
|
-
import { Button } from "../ui/button";
|
|
15
|
-
import { Badge } from "../ui/badge";
|
|
16
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
17
|
-
import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
|
|
18
|
-
import { cn } from "../../lib/utils";
|
|
19
|
-
import { fetchJsonWithTimeout } from "../../lib/apiClient";
|
|
20
|
-
import {
|
|
21
|
-
DeleteInspectorGroupResponseSchema,
|
|
22
|
-
DeleteInspectorGroupsResponseSchema,
|
|
23
|
-
GroupEvidenceExportResultSchema,
|
|
24
|
-
type GroupEvidenceReadResponse,
|
|
25
|
-
type GroupEvidenceExportResult,
|
|
26
|
-
type InspectorGroup,
|
|
27
|
-
type InspectorGroupEvidence,
|
|
28
|
-
type InspectorGroupExportMember,
|
|
29
|
-
type InspectorGroupMember,
|
|
30
|
-
} from "../../lib/groupContract";
|
|
31
|
-
import type { EvidenceReport } from "../../lib/runContract";
|
|
32
|
-
import type { SessionLogSummary } from "../../lib/sessionInfoContract";
|
|
33
|
-
import { getSessionPath } from "../../lib/sessionUrl";
|
|
34
|
-
import { ClientLogo, clientAppLabel, detectClientApp } from "../clients/ClientLogo";
|
|
35
|
-
import { useGroupEvidence } from "../../lib/useGroupEvidence";
|
|
36
|
-
import { useGroups } from "../../lib/useGroups";
|
|
37
|
-
import { useCopyFeedback } from "../proxy-viewer/useCopyFeedback";
|
|
38
|
-
import { TransientToast, useTransientToast } from "../ui/transient-toast";
|
|
39
|
-
|
|
40
|
-
type MemberStatus = InspectorGroupMember["status"];
|
|
41
|
-
|
|
42
|
-
type GroupSummary = {
|
|
43
|
-
memberCount: number;
|
|
44
|
-
completedMembers: number;
|
|
45
|
-
failedMembers: number;
|
|
46
|
-
runningMembers: number;
|
|
47
|
-
missingStatusMembers: number;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type GroupMemberRow = {
|
|
51
|
-
member: InspectorGroupMember;
|
|
52
|
-
exportMember: InspectorGroupExportMember | null;
|
|
53
|
-
source: "group" | "export" | "evidence";
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const GROUP_OPERATION_TIMEOUT_MS = 20_000;
|
|
57
|
-
|
|
58
|
-
export function groupDeletePath(groupId: string, deleteEvidence: boolean): string {
|
|
59
|
-
const path = `/api/groups/${encodeURIComponent(groupId)}`;
|
|
60
|
-
return deleteEvidence ? `${path}?deleteEvidence=1` : path;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function groupsClearPath(deleteEvidence: boolean): string {
|
|
64
|
-
return deleteEvidence ? "/api/groups?deleteEvidence=1" : "/api/groups";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function summarizeGroup(group: InspectorGroup): GroupSummary {
|
|
68
|
-
let completedMembers = 0;
|
|
69
|
-
let failedMembers = 0;
|
|
70
|
-
let runningMembers = 0;
|
|
71
|
-
let missingStatusMembers = 0;
|
|
72
|
-
|
|
73
|
-
for (const member of group.members) {
|
|
74
|
-
switch (member.status) {
|
|
75
|
-
case "completed":
|
|
76
|
-
completedMembers += 1;
|
|
77
|
-
break;
|
|
78
|
-
case "failed":
|
|
79
|
-
case "cancelled":
|
|
80
|
-
failedMembers += 1;
|
|
81
|
-
break;
|
|
82
|
-
case "running":
|
|
83
|
-
runningMembers += 1;
|
|
84
|
-
break;
|
|
85
|
-
case "created":
|
|
86
|
-
break;
|
|
87
|
-
case null:
|
|
88
|
-
missingStatusMembers += 1;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
memberCount: group.members.length,
|
|
95
|
-
completedMembers,
|
|
96
|
-
failedMembers,
|
|
97
|
-
runningMembers,
|
|
98
|
-
missingStatusMembers,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function buildGroupMemberRows(
|
|
103
|
-
group: InspectorGroup,
|
|
104
|
-
exportResult: GroupEvidenceExportResult | null,
|
|
105
|
-
evidenceResponse: GroupEvidenceReadResponse | null = null,
|
|
106
|
-
): GroupMemberRow[] {
|
|
107
|
-
if (exportResult !== null && exportResult.group.id === group.id) {
|
|
108
|
-
return exportResult.members.map((exportMember) => ({
|
|
109
|
-
member: exportMember.member,
|
|
110
|
-
exportMember,
|
|
111
|
-
source: "export",
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
evidenceResponse !== null &&
|
|
117
|
-
evidenceResponse.group.id === group.id &&
|
|
118
|
-
evidenceResponse.members !== null
|
|
119
|
-
) {
|
|
120
|
-
return evidenceResponse.members.map((exportMember) => ({
|
|
121
|
-
member: exportMember.member,
|
|
122
|
-
exportMember,
|
|
123
|
-
source: "evidence",
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return group.members.map((member) => ({ member, exportMember: null, source: "group" }));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function statusClassName(status: InspectorGroup["status"] | MemberStatus): string {
|
|
131
|
-
switch (status) {
|
|
132
|
-
case "completed":
|
|
133
|
-
return "border-status-success/30 bg-status-success/10 text-status-success";
|
|
134
|
-
case "failed":
|
|
135
|
-
case "cancelled":
|
|
136
|
-
return "border-status-danger/30 bg-status-danger/10 text-status-danger";
|
|
137
|
-
case "running":
|
|
138
|
-
return "border-status-cool/30 bg-status-cool/10 text-status-cool";
|
|
139
|
-
case "created":
|
|
140
|
-
return "border-border bg-muted/30 text-muted-foreground";
|
|
141
|
-
case null:
|
|
142
|
-
return "border-border bg-muted/30 text-muted-foreground";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function statusLabel(status: InspectorGroup["status"] | MemberStatus): string {
|
|
147
|
-
return status ?? "unknown";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function reportStatusClassName(status: EvidenceReport["status"]): string {
|
|
151
|
-
switch (status) {
|
|
152
|
-
case "PASS":
|
|
153
|
-
return "border-status-success/30 bg-status-success/10 text-status-success";
|
|
154
|
-
case "FAIL":
|
|
155
|
-
return "border-status-danger/30 bg-status-danger/10 text-status-danger";
|
|
156
|
-
case "RUNNING":
|
|
157
|
-
return "border-status-cool/30 bg-status-cool/10 text-status-cool";
|
|
158
|
-
case "UNKNOWN":
|
|
159
|
-
return "border-border bg-muted/30 text-muted-foreground";
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function displayText(value: string | null): string {
|
|
164
|
-
if (value === null || value.trim().length === 0) return "-";
|
|
165
|
-
return value;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function formatNumber(value: number): string {
|
|
169
|
-
return value.toLocaleString();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function formatMs(value: number | null | undefined): string {
|
|
173
|
-
if (value === null || value === undefined) return "-";
|
|
174
|
-
if (value < 1000) return `${formatNumber(value)} ms`;
|
|
175
|
-
return `${(value / 1000).toFixed(1)} s`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function formatTimestamp(value: string): string {
|
|
179
|
-
const millis = Date.parse(value);
|
|
180
|
-
if (!Number.isFinite(millis)) return value;
|
|
181
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
182
|
-
month: "short",
|
|
183
|
-
day: "2-digit",
|
|
184
|
-
hour: "2-digit",
|
|
185
|
-
minute: "2-digit",
|
|
186
|
-
hour12: false,
|
|
187
|
-
}).format(new Date(millis));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function formatNullableTimestamp(value: string | null): string {
|
|
191
|
-
return value === null ? "-" : formatTimestamp(value);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function formatJsonPreview(value: unknown): string {
|
|
195
|
-
const serialized = JSON.stringify(value);
|
|
196
|
-
const text = serialized === undefined ? String(value) : serialized;
|
|
197
|
-
if (text.length <= 160) return text;
|
|
198
|
-
return `${text.slice(0, 157)}...`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function firstNonEmpty(...values: Array<string | null>): string {
|
|
202
|
-
for (const value of values) {
|
|
203
|
-
if (value !== null && value.trim().length > 0) return value;
|
|
204
|
-
}
|
|
205
|
-
return "-";
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function firstOptionalText(...values: Array<string | null | undefined>): string | null {
|
|
209
|
-
for (const value of values) {
|
|
210
|
-
if (value !== null && value !== undefined && value.trim().length > 0) return value;
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function providerModel(member: InspectorGroupMember): string {
|
|
216
|
-
const provider = member.provider;
|
|
217
|
-
const model = member.model;
|
|
218
|
-
|
|
219
|
-
if (provider !== null && provider.length > 0 && model !== null && model.length > 0) {
|
|
220
|
-
return `${provider} / ${model}`;
|
|
221
|
-
}
|
|
222
|
-
return firstNonEmpty(provider, model);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function selectedGroupFrom(
|
|
226
|
-
groups: InspectorGroup[],
|
|
227
|
-
selectedGroupId: string | null,
|
|
228
|
-
): InspectorGroup | null {
|
|
229
|
-
if (selectedGroupId !== null) {
|
|
230
|
-
const selected = groups.find((group) => group.id === selectedGroupId);
|
|
231
|
-
if (selected !== undefined) return selected;
|
|
232
|
-
}
|
|
233
|
-
return groups[0] ?? null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function latestLog(row: GroupMemberRow): SessionLogSummary | null {
|
|
237
|
-
const logs = row.exportMember?.session?.latestLogs ?? [];
|
|
238
|
-
return logs[0] ?? null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function memberSourceLabel(source: GroupMemberRow["source"]): string {
|
|
242
|
-
switch (source) {
|
|
243
|
-
case "export":
|
|
244
|
-
return "fresh export";
|
|
245
|
-
case "evidence":
|
|
246
|
-
return "evidence";
|
|
247
|
-
case "group":
|
|
248
|
-
return "group";
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function evidenceFor(
|
|
253
|
-
group: InspectorGroup,
|
|
254
|
-
exportResult: GroupEvidenceExportResult | null,
|
|
255
|
-
): InspectorGroupEvidence | null {
|
|
256
|
-
if (exportResult !== null && exportResult.group.id === group.id) return exportResult.evidence;
|
|
257
|
-
return group.evidence;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function exportSummaryFor(
|
|
261
|
-
group: InspectorGroup,
|
|
262
|
-
exportResult: GroupEvidenceExportResult | null,
|
|
263
|
-
evidenceResponse: GroupEvidenceReadResponse | null,
|
|
264
|
-
): GroupEvidenceExportResult["summary"] | null {
|
|
265
|
-
if (exportResult !== null && exportResult.group.id === group.id) return exportResult.summary;
|
|
266
|
-
if (evidenceResponse !== null && evidenceResponse.group.id === group.id) {
|
|
267
|
-
return evidenceResponse.summary;
|
|
268
|
-
}
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function evidenceReportFor(
|
|
273
|
-
group: InspectorGroup,
|
|
274
|
-
exportResult: GroupEvidenceExportResult | null,
|
|
275
|
-
evidenceResponse: GroupEvidenceReadResponse | null,
|
|
276
|
-
): EvidenceReport | null {
|
|
277
|
-
if (exportResult !== null && exportResult.group.id === group.id) {
|
|
278
|
-
return exportResult.jenkinsReport;
|
|
279
|
-
}
|
|
280
|
-
if (evidenceResponse !== null && evidenceResponse.group.id === group.id) {
|
|
281
|
-
return evidenceResponse.report;
|
|
282
|
-
}
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function GroupsDialog({
|
|
287
|
-
labelledTrigger = false,
|
|
288
|
-
}: {
|
|
289
|
-
labelledTrigger?: boolean;
|
|
290
|
-
}): JSX.Element {
|
|
291
|
-
const [open, setOpen] = useState(false);
|
|
292
|
-
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
|
293
|
-
const [exportingGroupId, setExportingGroupId] = useState<string | null>(null);
|
|
294
|
-
const [exportError, setExportError] = useState<string | null>(null);
|
|
295
|
-
const [exportResult, setExportResult] = useState<GroupEvidenceExportResult | null>(null);
|
|
296
|
-
const [deleteTarget, setDeleteTarget] = useState<InspectorGroup | null>(null);
|
|
297
|
-
const [deleteEvidence, setDeleteEvidence] = useState(false);
|
|
298
|
-
const [deletingGroupId, setDeletingGroupId] = useState<string | null>(null);
|
|
299
|
-
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
300
|
-
const [clearingGroups, setClearingGroups] = useState(false);
|
|
301
|
-
const { notice, showToast, dismissToast } = useTransientToast();
|
|
302
|
-
const { groups, isLoading, error, mutate } = useGroups();
|
|
303
|
-
|
|
304
|
-
const selectedGroup = useMemo(
|
|
305
|
-
() => selectedGroupFrom(groups, selectedGroupId),
|
|
306
|
-
[groups, selectedGroupId],
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
if (!open) return;
|
|
311
|
-
const hasSelected =
|
|
312
|
-
selectedGroupId !== null && groups.some((group) => group.id === selectedGroupId);
|
|
313
|
-
if (hasSelected) return;
|
|
314
|
-
setSelectedGroupId(groups[0]?.id ?? null);
|
|
315
|
-
}, [groups, open, selectedGroupId]);
|
|
316
|
-
|
|
317
|
-
const selectedExportResult =
|
|
318
|
-
selectedGroup !== null && exportResult !== null && exportResult.group.id === selectedGroup.id
|
|
319
|
-
? exportResult
|
|
320
|
-
: null;
|
|
321
|
-
const {
|
|
322
|
-
evidenceResponse,
|
|
323
|
-
isLoading: evidenceLoading,
|
|
324
|
-
error: evidenceError,
|
|
325
|
-
mutate: mutateEvidence,
|
|
326
|
-
} = useGroupEvidence(selectedGroup);
|
|
327
|
-
|
|
328
|
-
const handleRefresh = useCallback(() => {
|
|
329
|
-
void mutate();
|
|
330
|
-
}, [mutate]);
|
|
331
|
-
|
|
332
|
-
const handleClearGroups = useCallback(async () => {
|
|
333
|
-
if (groups.length === 0 || clearingGroups) return;
|
|
334
|
-
|
|
335
|
-
setClearingGroups(true);
|
|
336
|
-
setDeleteTarget(null);
|
|
337
|
-
setDeleteEvidence(false);
|
|
338
|
-
setDeleteError(null);
|
|
339
|
-
setExportError(null);
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const parsed = await fetchJsonWithTimeout(
|
|
343
|
-
groupsClearPath(false),
|
|
344
|
-
DeleteInspectorGroupsResponseSchema,
|
|
345
|
-
GROUP_OPERATION_TIMEOUT_MS,
|
|
346
|
-
{ method: "DELETE" },
|
|
347
|
-
(response) => `Clear failed with status ${String(response.status)}`,
|
|
348
|
-
);
|
|
349
|
-
setExportResult(null);
|
|
350
|
-
setSelectedGroupId(null);
|
|
351
|
-
await mutate();
|
|
352
|
-
await mutateEvidence();
|
|
353
|
-
showToast({
|
|
354
|
-
message: `Cleared ${formatNumber(parsed.deleted)} group${parsed.deleted === 1 ? "" : "s"}.`,
|
|
355
|
-
tone: "success",
|
|
356
|
-
});
|
|
357
|
-
} catch (err) {
|
|
358
|
-
showToast({
|
|
359
|
-
message: err instanceof Error ? err.message : "Unknown error clearing groups",
|
|
360
|
-
tone: "error",
|
|
361
|
-
});
|
|
362
|
-
} finally {
|
|
363
|
-
setClearingGroups(false);
|
|
364
|
-
}
|
|
365
|
-
}, [clearingGroups, groups.length, mutate, mutateEvidence, showToast]);
|
|
366
|
-
|
|
367
|
-
const handleExport = useCallback(
|
|
368
|
-
async (group: InspectorGroup) => {
|
|
369
|
-
setExportingGroupId(group.id);
|
|
370
|
-
setExportError(null);
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const parsed = await fetchJsonWithTimeout(
|
|
374
|
-
`/api/groups/${encodeURIComponent(group.id)}/evidence`,
|
|
375
|
-
GroupEvidenceExportResultSchema,
|
|
376
|
-
GROUP_OPERATION_TIMEOUT_MS,
|
|
377
|
-
{
|
|
378
|
-
method: "POST",
|
|
379
|
-
headers: { "content-type": "application/json" },
|
|
380
|
-
body: JSON.stringify({ latestLogLimit: 10 }),
|
|
381
|
-
},
|
|
382
|
-
(response) => `Export failed with status ${String(response.status)}`,
|
|
383
|
-
);
|
|
384
|
-
setExportResult(parsed);
|
|
385
|
-
await mutate();
|
|
386
|
-
await mutateEvidence();
|
|
387
|
-
} catch (err) {
|
|
388
|
-
setExportError(err instanceof Error ? err.message : String(err));
|
|
389
|
-
} finally {
|
|
390
|
-
setExportingGroupId(null);
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
[mutate, mutateEvidence],
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
const handleRequestDelete = useCallback((group: InspectorGroup) => {
|
|
397
|
-
setDeleteTarget(group);
|
|
398
|
-
setDeleteEvidence(false);
|
|
399
|
-
setDeleteError(null);
|
|
400
|
-
}, []);
|
|
401
|
-
|
|
402
|
-
const handleCancelDelete = useCallback(() => {
|
|
403
|
-
if (deletingGroupId !== null) return;
|
|
404
|
-
setDeleteTarget(null);
|
|
405
|
-
setDeleteEvidence(false);
|
|
406
|
-
setDeleteError(null);
|
|
407
|
-
}, [deletingGroupId]);
|
|
408
|
-
|
|
409
|
-
const handleSelectGroup = useCallback((groupId: string) => {
|
|
410
|
-
setSelectedGroupId(groupId);
|
|
411
|
-
setDeleteTarget(null);
|
|
412
|
-
setDeleteEvidence(false);
|
|
413
|
-
setDeleteError(null);
|
|
414
|
-
}, []);
|
|
415
|
-
|
|
416
|
-
const handleDelete = useCallback(async () => {
|
|
417
|
-
const group = deleteTarget;
|
|
418
|
-
if (group === null) return;
|
|
419
|
-
|
|
420
|
-
setDeletingGroupId(group.id);
|
|
421
|
-
setDeleteError(null);
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
await fetchJsonWithTimeout(
|
|
425
|
-
groupDeletePath(group.id, deleteEvidence),
|
|
426
|
-
DeleteInspectorGroupResponseSchema,
|
|
427
|
-
GROUP_OPERATION_TIMEOUT_MS,
|
|
428
|
-
{ method: "DELETE" },
|
|
429
|
-
(response) => `Delete failed with status ${String(response.status)}`,
|
|
430
|
-
);
|
|
431
|
-
setExportResult((current) =>
|
|
432
|
-
current !== null && current.group.id === group.id ? null : current,
|
|
433
|
-
);
|
|
434
|
-
setSelectedGroupId((current) => (current === group.id ? null : current));
|
|
435
|
-
setDeleteTarget(null);
|
|
436
|
-
setDeleteEvidence(false);
|
|
437
|
-
await mutate();
|
|
438
|
-
await mutateEvidence();
|
|
439
|
-
} catch (err) {
|
|
440
|
-
setDeleteError(err instanceof Error ? err.message : String(err));
|
|
441
|
-
} finally {
|
|
442
|
-
setDeletingGroupId(null);
|
|
443
|
-
}
|
|
444
|
-
}, [deleteEvidence, deleteTarget, mutate, mutateEvidence]);
|
|
445
|
-
|
|
446
|
-
return (
|
|
447
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
448
|
-
<TransientToast notice={notice} onDismiss={dismissToast} />
|
|
449
|
-
<DialogTrigger asChild>
|
|
450
|
-
<Button
|
|
451
|
-
type="button"
|
|
452
|
-
variant="ghost"
|
|
453
|
-
size={labelledTrigger ? "default" : "icon"}
|
|
454
|
-
className={cn(
|
|
455
|
-
INSPECTOR_ICON_TRIGGER_CLASS,
|
|
456
|
-
labelledTrigger && "w-auto gap-2 px-3 text-xs max-lg:h-11 max-lg:w-auto",
|
|
457
|
-
)}
|
|
458
|
-
aria-label="Evaluation groups"
|
|
459
|
-
title="Evaluation groups"
|
|
460
|
-
>
|
|
461
|
-
<Layers className="size-4" />
|
|
462
|
-
{isLoading ? (
|
|
463
|
-
<Loader2 className="absolute -right-0.5 -top-0.5 size-3 animate-spin text-muted-foreground" />
|
|
464
|
-
) : groups.length > 0 ? (
|
|
465
|
-
<span className="absolute -right-1 -top-1 min-w-4 rounded-full bg-primary px-1 text-[10px] leading-4 text-primary-foreground">
|
|
466
|
-
{groups.length}
|
|
467
|
-
</span>
|
|
468
|
-
) : null}
|
|
469
|
-
<span className={labelledTrigger ? "" : "sr-only"}>Groups</span>
|
|
470
|
-
</Button>
|
|
471
|
-
</DialogTrigger>
|
|
472
|
-
<DialogContent className="flex max-h-[85vh] max-w-5xl flex-col overflow-hidden">
|
|
473
|
-
<DialogHeader>
|
|
474
|
-
<DialogTitle className="flex items-center gap-2 pr-8">
|
|
475
|
-
<Layers className="size-5 text-muted-foreground" />
|
|
476
|
-
<span>Groups</span>
|
|
477
|
-
<Badge variant="outline">{formatNumber(groups.length)}</Badge>
|
|
478
|
-
</DialogTitle>
|
|
479
|
-
</DialogHeader>
|
|
480
|
-
|
|
481
|
-
<div className="flex min-h-0 items-center justify-between gap-3 border-b border-border pb-3">
|
|
482
|
-
<p className="text-xs text-muted-foreground">
|
|
483
|
-
Evaluation batches, model runs, and exported evidence packs.
|
|
484
|
-
</p>
|
|
485
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
486
|
-
<Button
|
|
487
|
-
type="button"
|
|
488
|
-
variant="outline"
|
|
489
|
-
size="sm"
|
|
490
|
-
disabled={groups.length === 0 || clearingGroups}
|
|
491
|
-
onClick={() => void handleClearGroups()}
|
|
492
|
-
title="Clear all groups. Session logs and run records stay intact."
|
|
493
|
-
>
|
|
494
|
-
{clearingGroups ? (
|
|
495
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
496
|
-
) : (
|
|
497
|
-
<Trash2 className="size-3.5" />
|
|
498
|
-
)}
|
|
499
|
-
Clear
|
|
500
|
-
</Button>
|
|
501
|
-
<Button type="button" variant="outline" size="sm" onClick={handleRefresh}>
|
|
502
|
-
<RefreshCw className="size-3.5" />
|
|
503
|
-
Refresh
|
|
504
|
-
</Button>
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<div className="grid min-h-0 flex-1 gap-4 md:grid-cols-[260px_minmax(0,1fr)]">
|
|
509
|
-
<GroupList
|
|
510
|
-
groups={groups}
|
|
511
|
-
selectedGroupId={selectedGroup?.id ?? null}
|
|
512
|
-
isLoading={isLoading}
|
|
513
|
-
error={error}
|
|
514
|
-
onSelect={handleSelectGroup}
|
|
515
|
-
/>
|
|
516
|
-
<GroupDetails
|
|
517
|
-
group={selectedGroup}
|
|
518
|
-
deleteTarget={deleteTarget}
|
|
519
|
-
deleteEvidence={deleteEvidence}
|
|
520
|
-
exportResult={selectedExportResult}
|
|
521
|
-
evidenceResponse={evidenceResponse}
|
|
522
|
-
evidenceLoading={evidenceLoading}
|
|
523
|
-
evidenceError={evidenceError}
|
|
524
|
-
exportError={exportError}
|
|
525
|
-
deleteError={deleteError}
|
|
526
|
-
exportingGroupId={exportingGroupId}
|
|
527
|
-
deletingGroupId={deletingGroupId}
|
|
528
|
-
onDeleteEvidenceChange={setDeleteEvidence}
|
|
529
|
-
onExport={handleExport}
|
|
530
|
-
onRequestDelete={handleRequestDelete}
|
|
531
|
-
onCancelDelete={handleCancelDelete}
|
|
532
|
-
onConfirmDelete={() => void handleDelete()}
|
|
533
|
-
/>
|
|
534
|
-
</div>
|
|
535
|
-
</DialogContent>
|
|
536
|
-
</Dialog>
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function GroupList({
|
|
541
|
-
groups,
|
|
542
|
-
selectedGroupId,
|
|
543
|
-
isLoading,
|
|
544
|
-
error,
|
|
545
|
-
onSelect,
|
|
546
|
-
}: {
|
|
547
|
-
groups: InspectorGroup[];
|
|
548
|
-
selectedGroupId: string | null;
|
|
549
|
-
isLoading: boolean;
|
|
550
|
-
error: Error | undefined;
|
|
551
|
-
onSelect: (groupId: string) => void;
|
|
552
|
-
}): JSX.Element {
|
|
553
|
-
if (isLoading) {
|
|
554
|
-
return (
|
|
555
|
-
<div className="flex min-h-48 items-center justify-center rounded-md border border-dashed border-border text-xs text-muted-foreground">
|
|
556
|
-
Loading groups...
|
|
557
|
-
</div>
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (error !== undefined) {
|
|
562
|
-
return (
|
|
563
|
-
<div className="rounded-md border border-status-danger/30 bg-status-danger/10 px-3 py-2 text-xs text-status-danger">
|
|
564
|
-
{error.message}
|
|
565
|
-
</div>
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (groups.length === 0) {
|
|
570
|
-
return (
|
|
571
|
-
<div className="rounded-md border border-dashed border-border px-3 py-8 text-center text-xs text-muted-foreground">
|
|
572
|
-
No groups yet.
|
|
573
|
-
</div>
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return (
|
|
578
|
-
<div className="min-h-0 overflow-y-auto pr-1">
|
|
579
|
-
<div className="space-y-2">
|
|
580
|
-
{groups.map((group) => {
|
|
581
|
-
const summary = summarizeGroup(group);
|
|
582
|
-
const selected = selectedGroupId === group.id;
|
|
583
|
-
return (
|
|
584
|
-
<button
|
|
585
|
-
key={group.id}
|
|
586
|
-
type="button"
|
|
587
|
-
onClick={() => onSelect(group.id)}
|
|
588
|
-
className={cn(
|
|
589
|
-
"w-full rounded-md border px-3 py-2 text-left text-xs transition-colors",
|
|
590
|
-
selected
|
|
591
|
-
? "border-primary bg-primary/5"
|
|
592
|
-
: "border-border bg-background hover:bg-muted/50",
|
|
593
|
-
)}
|
|
594
|
-
>
|
|
595
|
-
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
596
|
-
<span className="min-w-0 text-sm font-medium leading-snug">{group.title}</span>
|
|
597
|
-
<Badge variant="outline" className={statusClassName(group.status)}>
|
|
598
|
-
{statusLabel(group.status)}
|
|
599
|
-
</Badge>
|
|
600
|
-
</div>
|
|
601
|
-
<div className="mt-1 flex items-center gap-2 text-muted-foreground">
|
|
602
|
-
<span>{summary.memberCount} sessions</span>
|
|
603
|
-
<span>{formatTimestamp(group.updatedAt)}</span>
|
|
604
|
-
</div>
|
|
605
|
-
</button>
|
|
606
|
-
);
|
|
607
|
-
})}
|
|
608
|
-
</div>
|
|
609
|
-
</div>
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function GroupDetails({
|
|
614
|
-
group,
|
|
615
|
-
deleteTarget,
|
|
616
|
-
deleteEvidence,
|
|
617
|
-
exportResult,
|
|
618
|
-
evidenceResponse,
|
|
619
|
-
evidenceLoading,
|
|
620
|
-
evidenceError,
|
|
621
|
-
exportError,
|
|
622
|
-
deleteError,
|
|
623
|
-
exportingGroupId,
|
|
624
|
-
deletingGroupId,
|
|
625
|
-
onDeleteEvidenceChange,
|
|
626
|
-
onExport,
|
|
627
|
-
onRequestDelete,
|
|
628
|
-
onCancelDelete,
|
|
629
|
-
onConfirmDelete,
|
|
630
|
-
}: {
|
|
631
|
-
group: InspectorGroup | null;
|
|
632
|
-
deleteTarget: InspectorGroup | null;
|
|
633
|
-
deleteEvidence: boolean;
|
|
634
|
-
exportResult: GroupEvidenceExportResult | null;
|
|
635
|
-
evidenceResponse: GroupEvidenceReadResponse | null;
|
|
636
|
-
evidenceLoading: boolean;
|
|
637
|
-
evidenceError: Error | undefined;
|
|
638
|
-
exportError: string | null;
|
|
639
|
-
deleteError: string | null;
|
|
640
|
-
exportingGroupId: string | null;
|
|
641
|
-
deletingGroupId: string | null;
|
|
642
|
-
onDeleteEvidenceChange: (deleteEvidence: boolean) => void;
|
|
643
|
-
onExport: (group: InspectorGroup) => Promise<void>;
|
|
644
|
-
onRequestDelete: (group: InspectorGroup) => void;
|
|
645
|
-
onCancelDelete: () => void;
|
|
646
|
-
onConfirmDelete: () => void;
|
|
647
|
-
}): JSX.Element {
|
|
648
|
-
if (group === null) {
|
|
649
|
-
return (
|
|
650
|
-
<div className="flex min-h-48 items-center justify-center rounded-md border border-dashed border-border text-xs text-muted-foreground">
|
|
651
|
-
Select a group to inspect.
|
|
652
|
-
</div>
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const summary = summarizeGroup(group);
|
|
657
|
-
const exportedSummary = exportSummaryFor(group, exportResult, evidenceResponse);
|
|
658
|
-
const evidenceReport = evidenceReportFor(group, exportResult, evidenceResponse);
|
|
659
|
-
const evidence = evidenceFor(group, exportResult);
|
|
660
|
-
const rows = buildGroupMemberRows(group, exportResult, evidenceResponse);
|
|
661
|
-
const isExporting = exportingGroupId === group.id;
|
|
662
|
-
const isDeleting = deletingGroupId === group.id;
|
|
663
|
-
const isConfirmingDelete = deleteTarget?.id === group.id;
|
|
664
|
-
|
|
665
|
-
return (
|
|
666
|
-
<div className="min-h-0 overflow-y-auto pr-1">
|
|
667
|
-
<div className="space-y-4">
|
|
668
|
-
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
669
|
-
<div className="min-w-0 space-y-1">
|
|
670
|
-
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
671
|
-
<h3 className="min-w-0 text-base font-semibold leading-snug">{group.title}</h3>
|
|
672
|
-
<Badge variant="outline" className={statusClassName(group.status)}>
|
|
673
|
-
{statusLabel(group.status)}
|
|
674
|
-
</Badge>
|
|
675
|
-
<Badge variant="outline">{group.kind}</Badge>
|
|
676
|
-
</div>
|
|
677
|
-
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
678
|
-
<span>{displayText(group.project)}</span>
|
|
679
|
-
<span>updated {formatTimestamp(group.updatedAt)}</span>
|
|
680
|
-
</div>
|
|
681
|
-
</div>
|
|
682
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
683
|
-
<Button
|
|
684
|
-
type="button"
|
|
685
|
-
variant="outline"
|
|
686
|
-
size="icon"
|
|
687
|
-
className="size-8 text-status-danger hover:text-status-danger/80"
|
|
688
|
-
aria-label="Delete group"
|
|
689
|
-
title="Delete group"
|
|
690
|
-
disabled={isDeleting}
|
|
691
|
-
onClick={() => onRequestDelete(group)}
|
|
692
|
-
>
|
|
693
|
-
{isDeleting ? (
|
|
694
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
695
|
-
) : (
|
|
696
|
-
<Trash2 className="size-3.5" />
|
|
697
|
-
)}
|
|
698
|
-
</Button>
|
|
699
|
-
<Button
|
|
700
|
-
type="button"
|
|
701
|
-
variant="default"
|
|
702
|
-
size="sm"
|
|
703
|
-
className="shrink-0"
|
|
704
|
-
disabled={isExporting || isDeleting}
|
|
705
|
-
onClick={() => void onExport(group)}
|
|
706
|
-
>
|
|
707
|
-
{isExporting ? (
|
|
708
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
709
|
-
) : (
|
|
710
|
-
<Download className="size-3.5" />
|
|
711
|
-
)}
|
|
712
|
-
Export evidence
|
|
713
|
-
</Button>
|
|
714
|
-
</div>
|
|
715
|
-
</div>
|
|
716
|
-
|
|
717
|
-
{group.task !== null && group.task.trim().length > 0 && (
|
|
718
|
-
<p className="rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
|
719
|
-
{group.task}
|
|
720
|
-
</p>
|
|
721
|
-
)}
|
|
722
|
-
|
|
723
|
-
{isConfirmingDelete && (
|
|
724
|
-
<DeleteGroupPanel
|
|
725
|
-
group={group}
|
|
726
|
-
deleteEvidence={deleteEvidence}
|
|
727
|
-
isDeleting={isDeleting}
|
|
728
|
-
error={deleteError}
|
|
729
|
-
onDeleteEvidenceChange={onDeleteEvidenceChange}
|
|
730
|
-
onCancel={onCancelDelete}
|
|
731
|
-
onConfirm={onConfirmDelete}
|
|
732
|
-
/>
|
|
733
|
-
)}
|
|
734
|
-
|
|
735
|
-
<div className="grid gap-2 sm:grid-cols-4">
|
|
736
|
-
<SummaryStat label="Members" value={summary.memberCount} />
|
|
737
|
-
<SummaryStat label="Done" value={summary.completedMembers} />
|
|
738
|
-
<SummaryStat label="Failed" value={summary.failedMembers} />
|
|
739
|
-
<SummaryStat label="Running" value={summary.runningMembers} />
|
|
740
|
-
</div>
|
|
741
|
-
|
|
742
|
-
{exportedSummary !== null && (
|
|
743
|
-
<div className="grid gap-2 sm:grid-cols-4">
|
|
744
|
-
<SummaryStat label="Requests" value={exportedSummary.requestCount} />
|
|
745
|
-
<SummaryStat label="Errors" value={exportedSummary.errorCount} />
|
|
746
|
-
<SummaryStat label="Tokens" value={exportedSummary.tokenUsage.total} />
|
|
747
|
-
<SummaryStat label="Missing" value={exportedSummary.missingSessions} />
|
|
748
|
-
</div>
|
|
749
|
-
)}
|
|
750
|
-
|
|
751
|
-
<EvidenceReportCard
|
|
752
|
-
report={evidenceReport}
|
|
753
|
-
loading={evidenceLoading}
|
|
754
|
-
error={evidenceError}
|
|
755
|
-
hasEvidence={evidence !== null}
|
|
756
|
-
/>
|
|
757
|
-
|
|
758
|
-
<EvidencePaths evidence={evidence} />
|
|
759
|
-
|
|
760
|
-
{exportError !== null && (
|
|
761
|
-
<div className="rounded-md border border-status-danger/30 bg-status-danger/10 px-3 py-2 text-xs text-status-danger">
|
|
762
|
-
{exportError}
|
|
763
|
-
</div>
|
|
764
|
-
)}
|
|
765
|
-
|
|
766
|
-
{deleteError !== null && !isConfirmingDelete && (
|
|
767
|
-
<div className="rounded-md border border-status-danger/30 bg-status-danger/10 px-3 py-2 text-xs text-status-danger">
|
|
768
|
-
{deleteError}
|
|
769
|
-
</div>
|
|
770
|
-
)}
|
|
771
|
-
|
|
772
|
-
<MemberTable rows={rows} />
|
|
773
|
-
</div>
|
|
774
|
-
</div>
|
|
775
|
-
);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function DeleteGroupPanel({
|
|
779
|
-
group,
|
|
780
|
-
deleteEvidence,
|
|
781
|
-
isDeleting,
|
|
782
|
-
error,
|
|
783
|
-
onDeleteEvidenceChange,
|
|
784
|
-
onCancel,
|
|
785
|
-
onConfirm,
|
|
786
|
-
}: {
|
|
787
|
-
group: InspectorGroup;
|
|
788
|
-
deleteEvidence: boolean;
|
|
789
|
-
isDeleting: boolean;
|
|
790
|
-
error: string | null;
|
|
791
|
-
onDeleteEvidenceChange: (deleteEvidence: boolean) => void;
|
|
792
|
-
onCancel: () => void;
|
|
793
|
-
onConfirm: () => void;
|
|
794
|
-
}): JSX.Element {
|
|
795
|
-
const hasEvidence = group.evidence !== null;
|
|
796
|
-
const evidencePath = group.evidence?.markdownPath ?? "No evidence pack exported.";
|
|
797
|
-
|
|
798
|
-
return (
|
|
799
|
-
<div className="rounded-md border border-status-danger/30 bg-status-danger/10 px-3 py-3 text-status-danger">
|
|
800
|
-
<div className="flex min-w-0 flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
801
|
-
<div className="min-w-0 space-y-2">
|
|
802
|
-
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
803
|
-
<Trash2 className="size-4 text-status-danger" />
|
|
804
|
-
<span>Delete group?</span>
|
|
805
|
-
</div>
|
|
806
|
-
<p className="text-xs text-status-danger">
|
|
807
|
-
Delete <span className="font-medium">{group.title}</span>. Session logs and run records
|
|
808
|
-
stay intact.
|
|
809
|
-
</p>
|
|
810
|
-
<label
|
|
811
|
-
className={cn(
|
|
812
|
-
"flex min-w-0 items-start gap-2 text-xs",
|
|
813
|
-
hasEvidence ? "cursor-pointer" : "opacity-60",
|
|
814
|
-
)}
|
|
815
|
-
>
|
|
816
|
-
<input
|
|
817
|
-
type="checkbox"
|
|
818
|
-
className="mt-0.5 size-4 shrink-0"
|
|
819
|
-
checked={deleteEvidence}
|
|
820
|
-
disabled={!hasEvidence || isDeleting}
|
|
821
|
-
onChange={(event) => onDeleteEvidenceChange(event.currentTarget.checked)}
|
|
822
|
-
/>
|
|
823
|
-
<span className="min-w-0">
|
|
824
|
-
<span className="block font-medium">Also delete exported evidence files</span>
|
|
825
|
-
<code className="mt-1 block truncate font-mono text-[11px] text-status-danger">
|
|
826
|
-
{evidencePath}
|
|
827
|
-
</code>
|
|
828
|
-
</span>
|
|
829
|
-
</label>
|
|
830
|
-
{error !== null && (
|
|
831
|
-
<div className="rounded-md border border-status-danger/30 bg-card px-2 py-1.5 text-xs text-status-danger">
|
|
832
|
-
{error}
|
|
833
|
-
</div>
|
|
834
|
-
)}
|
|
835
|
-
</div>
|
|
836
|
-
<div className="flex shrink-0 justify-end gap-2">
|
|
837
|
-
<Button
|
|
838
|
-
type="button"
|
|
839
|
-
variant="outline"
|
|
840
|
-
size="sm"
|
|
841
|
-
disabled={isDeleting}
|
|
842
|
-
onClick={onCancel}
|
|
843
|
-
>
|
|
844
|
-
Cancel
|
|
845
|
-
</Button>
|
|
846
|
-
<Button
|
|
847
|
-
type="button"
|
|
848
|
-
variant="destructive"
|
|
849
|
-
size="sm"
|
|
850
|
-
disabled={isDeleting}
|
|
851
|
-
onClick={onConfirm}
|
|
852
|
-
>
|
|
853
|
-
{isDeleting ? (
|
|
854
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
855
|
-
) : (
|
|
856
|
-
<Trash2 className="size-3.5" />
|
|
857
|
-
)}
|
|
858
|
-
Delete
|
|
859
|
-
</Button>
|
|
860
|
-
</div>
|
|
861
|
-
</div>
|
|
862
|
-
</div>
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function SummaryStat({ label, value }: { label: string; value: number }): JSX.Element {
|
|
867
|
-
return (
|
|
868
|
-
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
869
|
-
<div className="text-[11px] font-medium uppercase text-muted-foreground">{label}</div>
|
|
870
|
-
<div className="mt-1 font-mono text-sm font-semibold">{formatNumber(value)}</div>
|
|
871
|
-
</div>
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function EvidenceReportCard({
|
|
876
|
-
report,
|
|
877
|
-
loading,
|
|
878
|
-
error,
|
|
879
|
-
hasEvidence,
|
|
880
|
-
}: {
|
|
881
|
-
report: EvidenceReport | null;
|
|
882
|
-
loading: boolean;
|
|
883
|
-
error: Error | undefined;
|
|
884
|
-
hasEvidence: boolean;
|
|
885
|
-
}): JSX.Element | null {
|
|
886
|
-
const { copied, copy } = useCopyFeedback(report?.markdown ?? null);
|
|
887
|
-
|
|
888
|
-
if (report === null && !loading && error === undefined) return null;
|
|
889
|
-
|
|
890
|
-
if (loading) {
|
|
891
|
-
return (
|
|
892
|
-
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
|
893
|
-
<Loader2 className="size-3.5 animate-spin" />
|
|
894
|
-
Loading evidence report...
|
|
895
|
-
</div>
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (error !== undefined && hasEvidence) {
|
|
900
|
-
return (
|
|
901
|
-
<div className="rounded-md border border-status-danger/30 bg-status-danger/10 px-3 py-2 text-xs text-status-danger">
|
|
902
|
-
Failed to load evidence report: {error.message}
|
|
903
|
-
</div>
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (report === null) return null;
|
|
908
|
-
|
|
909
|
-
return (
|
|
910
|
-
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
911
|
-
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
912
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
913
|
-
<span className="text-xs font-semibold">Evidence report</span>
|
|
914
|
-
<Badge variant="outline" className={reportStatusClassName(report.status)}>
|
|
915
|
-
{report.status}
|
|
916
|
-
</Badge>
|
|
917
|
-
</div>
|
|
918
|
-
<Button
|
|
919
|
-
type="button"
|
|
920
|
-
variant="ghost"
|
|
921
|
-
size="icon"
|
|
922
|
-
className="size-7 shrink-0"
|
|
923
|
-
aria-label={copied ? "Copied evidence report" : "Copy evidence report"}
|
|
924
|
-
onClick={copy}
|
|
925
|
-
>
|
|
926
|
-
{copied ? (
|
|
927
|
-
<Check className="size-3.5 text-status-success" />
|
|
928
|
-
) : (
|
|
929
|
-
<Copy className="size-3.5" />
|
|
930
|
-
)}
|
|
931
|
-
</Button>
|
|
932
|
-
</div>
|
|
933
|
-
<p className="mt-1 text-xs text-muted-foreground">{report.summary}</p>
|
|
934
|
-
</div>
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function EvidencePaths({ evidence }: { evidence: InspectorGroupEvidence | null }): JSX.Element {
|
|
939
|
-
if (evidence === null) {
|
|
940
|
-
return (
|
|
941
|
-
<div className="rounded-md border border-dashed border-border px-3 py-2 text-xs text-muted-foreground">
|
|
942
|
-
No evidence pack exported yet.
|
|
943
|
-
</div>
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
return (
|
|
948
|
-
<div className="space-y-2 rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
949
|
-
<div className="flex items-center justify-between gap-2">
|
|
950
|
-
<span className="text-xs font-semibold">Evidence paths</span>
|
|
951
|
-
<span className="text-[11px] text-muted-foreground">
|
|
952
|
-
{formatTimestamp(evidence.exportedAt)}
|
|
953
|
-
</span>
|
|
954
|
-
</div>
|
|
955
|
-
<div className="grid gap-2">
|
|
956
|
-
<CopyPathRow label="JSON" value={evidence.jsonPath} />
|
|
957
|
-
<CopyPathRow label="Markdown" value={evidence.markdownPath} />
|
|
958
|
-
<CopyPathRow label="HTML" value={evidence.htmlPath} />
|
|
959
|
-
</div>
|
|
960
|
-
</div>
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function CopyPathRow({ label, value }: { label: string; value: string }): JSX.Element {
|
|
965
|
-
const { copied, copy } = useCopyFeedback(value);
|
|
966
|
-
return (
|
|
967
|
-
<div className="flex min-w-0 items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5">
|
|
968
|
-
<span className="w-16 shrink-0 text-[11px] font-medium text-muted-foreground">{label}</span>
|
|
969
|
-
<code className="min-w-0 flex-1 truncate font-mono text-xs">{value}</code>
|
|
970
|
-
<Button
|
|
971
|
-
type="button"
|
|
972
|
-
variant="ghost"
|
|
973
|
-
size="icon"
|
|
974
|
-
className="size-7 shrink-0"
|
|
975
|
-
aria-label={copied ? `Copied ${label} path` : `Copy ${label} path`}
|
|
976
|
-
onClick={copy}
|
|
977
|
-
>
|
|
978
|
-
{copied ? (
|
|
979
|
-
<Check className="size-3.5 text-status-success" />
|
|
980
|
-
) : (
|
|
981
|
-
<Copy className="size-3.5" />
|
|
982
|
-
)}
|
|
983
|
-
</Button>
|
|
984
|
-
</div>
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function MemberTable({ rows }: { rows: GroupMemberRow[] }): JSX.Element {
|
|
989
|
-
const [expandedMemberId, setExpandedMemberId] = useState<string | null>(null);
|
|
990
|
-
|
|
991
|
-
const toggleMember = useCallback((memberId: string) => {
|
|
992
|
-
setExpandedMemberId((current) => (current === memberId ? null : memberId));
|
|
993
|
-
}, []);
|
|
994
|
-
|
|
995
|
-
if (rows.length === 0) {
|
|
996
|
-
return (
|
|
997
|
-
<div className="rounded-md border border-dashed border-border px-3 py-8 text-center text-xs text-muted-foreground">
|
|
998
|
-
No sessions attached.
|
|
999
|
-
</div>
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
return (
|
|
1004
|
-
<div className="overflow-hidden rounded-md border border-border">
|
|
1005
|
-
<div className="border-b border-border bg-muted/30 px-3 py-2 text-xs font-semibold">
|
|
1006
|
-
Members
|
|
1007
|
-
</div>
|
|
1008
|
-
<div className="overflow-x-auto">
|
|
1009
|
-
<table className="w-full min-w-[760px] text-left text-xs">
|
|
1010
|
-
<thead className="border-b border-border bg-muted/20 text-muted-foreground">
|
|
1011
|
-
<tr>
|
|
1012
|
-
<th className="px-3 py-2 font-medium">Session</th>
|
|
1013
|
-
<th className="px-3 py-2 font-medium">Provider / Model</th>
|
|
1014
|
-
<th className="px-3 py-2 font-medium">Status</th>
|
|
1015
|
-
<th className="px-3 py-2 font-medium">Requests</th>
|
|
1016
|
-
<th className="px-3 py-2 font-medium">Errors</th>
|
|
1017
|
-
<th className="px-3 py-2 font-medium">First chunk</th>
|
|
1018
|
-
<th className="px-3 py-2 font-medium">Tokens</th>
|
|
1019
|
-
</tr>
|
|
1020
|
-
</thead>
|
|
1021
|
-
<tbody>
|
|
1022
|
-
{rows.map((row) => {
|
|
1023
|
-
const isExpanded = expandedMemberId === row.member.id;
|
|
1024
|
-
return (
|
|
1025
|
-
<Fragment key={row.member.id}>
|
|
1026
|
-
<MemberRow row={row} isExpanded={isExpanded} onToggleDetails={toggleMember} />
|
|
1027
|
-
<MemberDetailsRow row={row} isExpanded={isExpanded} />
|
|
1028
|
-
</Fragment>
|
|
1029
|
-
);
|
|
1030
|
-
})}
|
|
1031
|
-
</tbody>
|
|
1032
|
-
</table>
|
|
1033
|
-
</div>
|
|
1034
|
-
</div>
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function MemberRow({
|
|
1039
|
-
row,
|
|
1040
|
-
isExpanded,
|
|
1041
|
-
onToggleDetails,
|
|
1042
|
-
}: {
|
|
1043
|
-
row: GroupMemberRow;
|
|
1044
|
-
isExpanded: boolean;
|
|
1045
|
-
onToggleDetails: (memberId: string) => void;
|
|
1046
|
-
}): JSX.Element {
|
|
1047
|
-
const member = row.member;
|
|
1048
|
-
const session = row.exportMember?.session ?? null;
|
|
1049
|
-
const log = latestLog(row);
|
|
1050
|
-
const sessionPath = session?.inspectorPath ?? getSessionPath(member.sessionId);
|
|
1051
|
-
const canOpenLiveSession = row.source !== "evidence";
|
|
1052
|
-
const requests = session?.requestCount ?? null;
|
|
1053
|
-
const errors = session?.errorCount ?? null;
|
|
1054
|
-
const tokens = session?.tokenUsage.total ?? null;
|
|
1055
|
-
const firstChunkMs = log?.firstChunkMs ?? null;
|
|
1056
|
-
const sessionLabel = firstNonEmpty(member.label, member.sessionId);
|
|
1057
|
-
const clientUserAgent = firstOptionalText(session?.clientUserAgent, log?.userAgent, member.agent);
|
|
1058
|
-
const clientProjectFolder = session?.clientProjectFolder ?? null;
|
|
1059
|
-
const clientApp = detectClientApp({
|
|
1060
|
-
userAgent: clientUserAgent,
|
|
1061
|
-
conversationId: member.sessionId,
|
|
1062
|
-
clientProjectFolder,
|
|
1063
|
-
});
|
|
1064
|
-
const clientLabel = clientAppLabel(clientApp);
|
|
1065
|
-
|
|
1066
|
-
return (
|
|
1067
|
-
<tr className="border-b border-border last:border-0 hover:bg-muted/20">
|
|
1068
|
-
<td className="max-w-56 px-3 py-2">
|
|
1069
|
-
<div className="flex min-w-0 items-center gap-1.5">
|
|
1070
|
-
<Button
|
|
1071
|
-
type="button"
|
|
1072
|
-
variant="ghost"
|
|
1073
|
-
size="icon"
|
|
1074
|
-
className="size-6 shrink-0 text-muted-foreground"
|
|
1075
|
-
aria-label={isExpanded ? `Hide member ${sessionLabel}` : `Show member ${sessionLabel}`}
|
|
1076
|
-
title={isExpanded ? "Hide member details" : "Show member details"}
|
|
1077
|
-
onClick={() => {
|
|
1078
|
-
onToggleDetails(member.id);
|
|
1079
|
-
}}
|
|
1080
|
-
>
|
|
1081
|
-
{isExpanded ? (
|
|
1082
|
-
<ChevronDown className="size-3.5" />
|
|
1083
|
-
) : (
|
|
1084
|
-
<ChevronRight className="size-3.5" />
|
|
1085
|
-
)}
|
|
1086
|
-
</Button>
|
|
1087
|
-
{clientLabel !== null && <ClientLogo client={clientApp} className="size-5" />}
|
|
1088
|
-
{canOpenLiveSession ? (
|
|
1089
|
-
<a
|
|
1090
|
-
href={sessionPath}
|
|
1091
|
-
target="_blank"
|
|
1092
|
-
rel="noreferrer"
|
|
1093
|
-
className="inline-flex max-w-full min-w-0 items-center gap-2 rounded-sm font-mono text-xs text-primary outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
1094
|
-
aria-label={`Open session ${sessionLabel}`}
|
|
1095
|
-
title={`Open session ${sessionLabel}`}
|
|
1096
|
-
>
|
|
1097
|
-
<span className="min-w-0 truncate">{sessionLabel}</span>
|
|
1098
|
-
<ExternalLink className="size-3 shrink-0 text-muted-foreground" />
|
|
1099
|
-
</a>
|
|
1100
|
-
) : (
|
|
1101
|
-
<button
|
|
1102
|
-
type="button"
|
|
1103
|
-
className="inline-flex max-w-full min-w-0 items-center gap-2 rounded-sm font-mono text-xs text-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
1104
|
-
title={`Show evidence snapshot for ${sessionLabel}`}
|
|
1105
|
-
aria-label={`Show evidence snapshot for ${sessionLabel}`}
|
|
1106
|
-
onClick={() => {
|
|
1107
|
-
onToggleDetails(member.id);
|
|
1108
|
-
}}
|
|
1109
|
-
>
|
|
1110
|
-
<span className="min-w-0 truncate">{sessionLabel}</span>
|
|
1111
|
-
<Badge
|
|
1112
|
-
variant="outline"
|
|
1113
|
-
className="shrink-0 border-border bg-muted/30 text-muted-foreground"
|
|
1114
|
-
>
|
|
1115
|
-
evidence
|
|
1116
|
-
</Badge>
|
|
1117
|
-
</button>
|
|
1118
|
-
)}
|
|
1119
|
-
</div>
|
|
1120
|
-
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
|
|
1121
|
-
{displayText(member.agent)}
|
|
1122
|
-
</div>
|
|
1123
|
-
</td>
|
|
1124
|
-
<td className="max-w-52 px-3 py-2">
|
|
1125
|
-
<span className="block whitespace-normal leading-snug">{providerModel(member)}</span>
|
|
1126
|
-
</td>
|
|
1127
|
-
<td className="px-3 py-2">
|
|
1128
|
-
<Badge variant="outline" className={statusClassName(member.status)}>
|
|
1129
|
-
{statusLabel(member.status)}
|
|
1130
|
-
</Badge>
|
|
1131
|
-
</td>
|
|
1132
|
-
<td className="px-3 py-2 font-mono">{requests === null ? "-" : formatNumber(requests)}</td>
|
|
1133
|
-
<td className="px-3 py-2 font-mono">{errors === null ? "-" : formatNumber(errors)}</td>
|
|
1134
|
-
<td className="px-3 py-2 font-mono">{formatMs(firstChunkMs)}</td>
|
|
1135
|
-
<td className="px-3 py-2 font-mono">{tokens === null ? "-" : formatNumber(tokens)}</td>
|
|
1136
|
-
</tr>
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function MemberDetailsRow({
|
|
1141
|
-
row,
|
|
1142
|
-
isExpanded,
|
|
1143
|
-
}: {
|
|
1144
|
-
row: GroupMemberRow;
|
|
1145
|
-
isExpanded: boolean;
|
|
1146
|
-
}): JSX.Element | null {
|
|
1147
|
-
if (!isExpanded) return null;
|
|
1148
|
-
|
|
1149
|
-
const member = row.member;
|
|
1150
|
-
const exportMember = row.exportMember;
|
|
1151
|
-
const session = exportMember?.session ?? null;
|
|
1152
|
-
const run = exportMember?.run ?? null;
|
|
1153
|
-
const metadataEntries = Object.entries(member.metadata);
|
|
1154
|
-
|
|
1155
|
-
return (
|
|
1156
|
-
<tr className="border-b border-border bg-muted/10">
|
|
1157
|
-
<td colSpan={7} className="px-3 pb-4 pt-2">
|
|
1158
|
-
<div className="space-y-3 rounded-md border border-border bg-background p-3">
|
|
1159
|
-
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
1160
|
-
<MemberDetailItem label="Source" value={memberSourceLabel(row.source)} />
|
|
1161
|
-
<MemberDetailItem label="Member ID" value={member.id} mono />
|
|
1162
|
-
<MemberDetailItem label="Session ID" value={member.sessionId} mono />
|
|
1163
|
-
<MemberDetailItem label="Run ID" value={run?.id ?? member.runId ?? "-"} mono />
|
|
1164
|
-
<MemberDetailItem label="Created" value={formatTimestamp(member.addedAt)} />
|
|
1165
|
-
<MemberDetailItem label="Updated" value={formatTimestamp(member.updatedAt)} />
|
|
1166
|
-
<MemberDetailItem
|
|
1167
|
-
label="Session created"
|
|
1168
|
-
value={formatNullableTimestamp(session?.createdAt ?? null)}
|
|
1169
|
-
/>
|
|
1170
|
-
<MemberDetailItem
|
|
1171
|
-
label="Session updated"
|
|
1172
|
-
value={formatNullableTimestamp(session?.updatedAt ?? null)}
|
|
1173
|
-
/>
|
|
1174
|
-
</div>
|
|
1175
|
-
|
|
1176
|
-
{session === null ? (
|
|
1177
|
-
<div className="rounded-md border border-dashed border-border px-3 py-2 text-xs text-muted-foreground">
|
|
1178
|
-
No session snapshot is available for this member.
|
|
1179
|
-
</div>
|
|
1180
|
-
) : (
|
|
1181
|
-
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
1182
|
-
<MemberDetailItem label="Log coverage" value={session.logCoverage} />
|
|
1183
|
-
<MemberDetailItem label="Runtime" value={displayText(session.runtimeMode)} />
|
|
1184
|
-
<MemberDetailItem label="Requests" value={formatNumber(session.requestCount)} mono />
|
|
1185
|
-
<MemberDetailItem label="Errors" value={formatNumber(session.errorCount)} mono />
|
|
1186
|
-
<MemberDetailItem
|
|
1187
|
-
label="Input tokens"
|
|
1188
|
-
value={formatNumber(session.tokenUsage.input)}
|
|
1189
|
-
mono
|
|
1190
|
-
/>
|
|
1191
|
-
<MemberDetailItem
|
|
1192
|
-
label="Output tokens"
|
|
1193
|
-
value={formatNumber(session.tokenUsage.output)}
|
|
1194
|
-
mono
|
|
1195
|
-
/>
|
|
1196
|
-
<MemberDetailItem
|
|
1197
|
-
label="Cache read"
|
|
1198
|
-
value={formatNumber(session.tokenUsage.cacheRead)}
|
|
1199
|
-
mono
|
|
1200
|
-
/>
|
|
1201
|
-
<MemberDetailItem
|
|
1202
|
-
label="Total tokens"
|
|
1203
|
-
value={formatNumber(session.tokenUsage.total)}
|
|
1204
|
-
mono
|
|
1205
|
-
/>
|
|
1206
|
-
</div>
|
|
1207
|
-
)}
|
|
1208
|
-
|
|
1209
|
-
<LatestLogsPreview logs={session?.latestLogs ?? []} />
|
|
1210
|
-
<MemberMetadata entries={metadataEntries} />
|
|
1211
|
-
</div>
|
|
1212
|
-
</td>
|
|
1213
|
-
</tr>
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function MemberDetailItem({
|
|
1218
|
-
label,
|
|
1219
|
-
value,
|
|
1220
|
-
mono = false,
|
|
1221
|
-
}: {
|
|
1222
|
-
label: string;
|
|
1223
|
-
value: string;
|
|
1224
|
-
mono?: boolean;
|
|
1225
|
-
}): JSX.Element {
|
|
1226
|
-
return (
|
|
1227
|
-
<div className="min-w-0">
|
|
1228
|
-
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
1229
|
-
{label}
|
|
1230
|
-
</div>
|
|
1231
|
-
<div
|
|
1232
|
-
className={cn("truncate text-xs text-foreground", mono ? "font-mono" : "")}
|
|
1233
|
-
title={value}
|
|
1234
|
-
>
|
|
1235
|
-
{value}
|
|
1236
|
-
</div>
|
|
1237
|
-
</div>
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function LatestLogsPreview({ logs }: { logs: SessionLogSummary[] }): JSX.Element {
|
|
1242
|
-
const visibleLogs = logs.slice(0, 5);
|
|
1243
|
-
|
|
1244
|
-
if (visibleLogs.length === 0) {
|
|
1245
|
-
return (
|
|
1246
|
-
<div className="rounded-md border border-dashed border-border px-3 py-2 text-xs text-muted-foreground">
|
|
1247
|
-
No latest logs in this snapshot.
|
|
1248
|
-
</div>
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
return (
|
|
1253
|
-
<div className="overflow-hidden rounded-md border border-border">
|
|
1254
|
-
<div className="border-b border-border bg-muted/30 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1255
|
-
Latest logs
|
|
1256
|
-
</div>
|
|
1257
|
-
<div className="divide-y divide-border">
|
|
1258
|
-
{visibleLogs.map((log) => {
|
|
1259
|
-
const displayNumber = log.sessionLogNumber ?? log.id;
|
|
1260
|
-
return (
|
|
1261
|
-
<div
|
|
1262
|
-
key={log.id}
|
|
1263
|
-
className="grid gap-2 px-3 py-2 text-xs sm:grid-cols-[72px_1fr_88px_88px]"
|
|
1264
|
-
>
|
|
1265
|
-
<span className="font-mono text-muted-foreground" title={`Log ID ${String(log.id)}`}>
|
|
1266
|
-
#{displayNumber}
|
|
1267
|
-
</span>
|
|
1268
|
-
<span className="min-w-0 truncate">
|
|
1269
|
-
{displayText(log.model)} · {log.apiFormat} · {log.path}
|
|
1270
|
-
</span>
|
|
1271
|
-
<span className="font-mono text-muted-foreground">{formatMs(log.firstChunkMs)}</span>
|
|
1272
|
-
<span className="font-mono text-muted-foreground">{formatMs(log.latencyMs)}</span>
|
|
1273
|
-
</div>
|
|
1274
|
-
);
|
|
1275
|
-
})}
|
|
1276
|
-
</div>
|
|
1277
|
-
</div>
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
function MemberMetadata({ entries }: { entries: Array<[string, unknown]> }): JSX.Element | null {
|
|
1282
|
-
if (entries.length === 0) return null;
|
|
1283
|
-
|
|
1284
|
-
return (
|
|
1285
|
-
<div className="overflow-hidden rounded-md border border-border">
|
|
1286
|
-
<div className="border-b border-border bg-muted/30 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1287
|
-
Metadata
|
|
1288
|
-
</div>
|
|
1289
|
-
<div className="grid gap-x-3 gap-y-2 p-3 text-xs sm:grid-cols-2">
|
|
1290
|
-
{entries.map(([key, value]) => {
|
|
1291
|
-
const preview = formatJsonPreview(value);
|
|
1292
|
-
return (
|
|
1293
|
-
<div key={key} className="min-w-0">
|
|
1294
|
-
<span className="mr-2 text-muted-foreground">{key}</span>
|
|
1295
|
-
<span className="font-mono text-foreground" title={preview}>
|
|
1296
|
-
{preview}
|
|
1297
|
-
</span>
|
|
1298
|
-
</div>
|
|
1299
|
-
);
|
|
1300
|
-
})}
|
|
1301
|
-
</div>
|
|
1302
|
-
</div>
|
|
1303
|
-
);
|
|
1304
|
-
}
|