@tonyclaw/agent-inspector 3.1.32 → 4.0.2
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 +241 -120
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-oXUk3MBR.mjs → router-BBY_zWRK.mjs} +91 -3
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-BtfPSpkV.js → CompareDrawer-rtNElN-B.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D9DPPQ3n.js → InspectorPet-QaAiuF3d.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Buu8vkCK.js → ProxyViewerContainer-6twNZHwl.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-BER-QzpD.js → ReplayDialog-B7KlziO2.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-C8Zwxrr7.js → RequestAnatomy-2XVFBrW4.js} +1 -1
- package/.output/ui/assets/{ResponseView-DDpKdYk6.js → ResponseView-BULgzLVB.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-sMPGG7Sw.js → StreamingChunkSequence-DSkvkbns.js} +1 -1
- package/.output/ui/assets/{_sessionId-e4IheHnP.js → _sessionId-D2dmJi3G.js} +1 -1
- package/.output/ui/assets/{_sessionId-C5Uv72Fx.js → _sessionId-DcS0s1Mr.js} +1 -1
- package/.output/ui/assets/{index-B__qT0Yd.js → index-B0TN1KKO.js} +2 -2
- package/.output/ui/assets/{index-Be3bTCzi.js → index-Bva1zHEs.js} +1 -1
- package/.output/ui/assets/{index-C9q__p8J.js → index-BzBExOxu.js} +1 -1
- package/.output/ui/assets/{index-CtB0deSZ.js → index-k6sRQz55.js} +1 -1
- package/.output/ui/assets/{json-viewer-Db0AFp-7.js → json-viewer-CWM4tY-j.js} +1 -1
- package/.output/ui/assets/{jszip.min-DkY0Ym_q.js → jszip.min-BkWvRFlo.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +8 -13
- 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 -1397
- 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 -253
- 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,2259 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ChangeEvent,
|
|
3
|
-
type JSX,
|
|
4
|
-
type PointerEvent as ReactPointerEvent,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
useCallback,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
Suspense,
|
|
12
|
-
} from "react";
|
|
13
|
-
import {
|
|
14
|
-
ALargeSmall,
|
|
15
|
-
ArrowDownRight,
|
|
16
|
-
ArrowLeft,
|
|
17
|
-
ArrowUpRight,
|
|
18
|
-
Clapperboard,
|
|
19
|
-
Check,
|
|
20
|
-
ChevronDown,
|
|
21
|
-
ChevronLeft,
|
|
22
|
-
ChevronRight,
|
|
23
|
-
ChevronUp,
|
|
24
|
-
ChevronsLeft,
|
|
25
|
-
ChevronsRight,
|
|
26
|
-
Copy,
|
|
27
|
-
Download,
|
|
28
|
-
Eye,
|
|
29
|
-
FileJson,
|
|
30
|
-
Gauge,
|
|
31
|
-
Flag,
|
|
32
|
-
PanelRightClose,
|
|
33
|
-
PanelRightOpen,
|
|
34
|
-
RefreshCw,
|
|
35
|
-
Siren,
|
|
36
|
-
Moon,
|
|
37
|
-
Sun,
|
|
38
|
-
Timer,
|
|
39
|
-
Trash2,
|
|
40
|
-
Upload,
|
|
41
|
-
X,
|
|
42
|
-
} from "lucide-react";
|
|
43
|
-
|
|
44
|
-
import type { CapturedLog } from "../contracts";
|
|
45
|
-
import { withBasePath } from "../lib/basePath";
|
|
46
|
-
import { exportLogsAsZip, type ExportMode } from "../lib/export-logs";
|
|
47
|
-
import {
|
|
48
|
-
subscribeCodingAgentEndpoints,
|
|
49
|
-
type CodingAgentEndpoints,
|
|
50
|
-
} from "../lib/codingAgentEndpoints";
|
|
51
|
-
import type { ImportLogsResponse } from "../lib/logImportContract";
|
|
52
|
-
import { copyTextToClipboard } from "../lib/clipboard";
|
|
53
|
-
import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
|
|
54
|
-
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
55
|
-
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
56
|
-
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
57
|
-
import { useThemeMode } from "../lib/useThemeMode";
|
|
58
|
-
import { formatUiScale, normalizeUiScalePreference, type UiScalePreference } from "../lib/uiScale";
|
|
59
|
-
import { useUiScale } from "../lib/useUiScale";
|
|
60
|
-
import { useProviders } from "../lib/useProviders";
|
|
61
|
-
import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
|
|
62
|
-
import {
|
|
63
|
-
buildSessionSlateStats,
|
|
64
|
-
buildHomeConnectionCommands,
|
|
65
|
-
COPYABLE_COMMAND_CONTAINER_CLASS_NAME,
|
|
66
|
-
COPYABLE_COMMAND_TEXT_CLASS_NAME,
|
|
67
|
-
exportRequiresConfirmation,
|
|
68
|
-
formatPaginationStatus,
|
|
69
|
-
getExportActionVisibility,
|
|
70
|
-
resolveSessionContextScope,
|
|
71
|
-
type SessionContextScope,
|
|
72
|
-
type SessionSlateMetricTone,
|
|
73
|
-
type SessionSlateStats,
|
|
74
|
-
type SessionSlateStatus,
|
|
75
|
-
} from "./proxy-viewer/proxyViewerLogic";
|
|
76
|
-
|
|
77
|
-
import { CrabLogo } from "./ui/crab-logo";
|
|
78
|
-
import { Button } from "./ui/button";
|
|
79
|
-
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
80
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
81
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
82
|
-
import type { TransientToastInput } from "./ui/transient-toast";
|
|
83
|
-
import { AlertsDialog } from "./alerts/AlertsDialog";
|
|
84
|
-
import { GroupsDialog } from "./groups/GroupsDialog";
|
|
85
|
-
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
86
|
-
import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
|
|
87
|
-
import { PiAgentPanel, type PiAgentSearchResult } from "./pi-agent/PiAgentPanel";
|
|
88
|
-
import { LazyFeatureBoundary } from "./errors/SafeErrorBoundary";
|
|
89
|
-
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
90
|
-
import { LazyCompareDrawer, LazyInspectorPet } from "./proxy-viewer/lazy";
|
|
91
|
-
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
92
|
-
import { ApplicationBar } from "./proxy-viewer/ApplicationBar";
|
|
93
|
-
import {
|
|
94
|
-
SessionLoadProgressBar,
|
|
95
|
-
type SessionLoadProgress,
|
|
96
|
-
} from "./proxy-viewer/SessionLoadProgressBar";
|
|
97
|
-
import { browserPrefersReducedMotion, preferredScrollBehavior } from "./proxy-viewer/accessibility";
|
|
98
|
-
import { LiveConnectionStatus } from "./proxy-viewer/LiveConnectionStatus";
|
|
99
|
-
import {
|
|
100
|
-
getLiveEmptyStateCopy,
|
|
101
|
-
resolveLiveEmptyState,
|
|
102
|
-
type LiveConnectionState,
|
|
103
|
-
} from "./proxy-viewer/liveConnectionState";
|
|
104
|
-
import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
|
|
105
|
-
import { RAW_EXPORT_DIALOG_CLASS_NAME, RawExportWarning } from "./proxy-viewer/RawExportWarning";
|
|
106
|
-
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
107
|
-
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
108
|
-
import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
|
|
109
|
-
import type {
|
|
110
|
-
ContextHealthLevel,
|
|
111
|
-
ContextRiskLevel,
|
|
112
|
-
} from "./proxy-viewer/anatomy/contextIntelligence";
|
|
113
|
-
import {
|
|
114
|
-
buildSessionContextSummary,
|
|
115
|
-
type SessionContextSummary,
|
|
116
|
-
type SessionModelContextSummary,
|
|
117
|
-
} from "./proxy-viewer/anatomy/sessionContextSummary";
|
|
118
|
-
|
|
119
|
-
function truncateSessionId(id: string): string {
|
|
120
|
-
if (id.length <= 30) return id;
|
|
121
|
-
return `${id.slice(0, 12)}...${id.slice(-12)}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function buildDisplayNumberByLogId(groups: readonly ConversationGroupData[]): Map<number, number> {
|
|
125
|
-
const result = new Map<number, number>();
|
|
126
|
-
for (const group of groups) {
|
|
127
|
-
for (let index = 0; index < group.logs.length; index += 1) {
|
|
128
|
-
const log = group.logs[index];
|
|
129
|
-
if (log === undefined) continue;
|
|
130
|
-
result.set(log.id, index + 1);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function displayNumberForLog(
|
|
137
|
-
log: CapturedLog,
|
|
138
|
-
displayNumberByLogId: ReadonlyMap<number, number>,
|
|
139
|
-
): number {
|
|
140
|
-
return displayNumberByLogId.get(log.id) ?? log.id;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
|
|
144
|
-
let totalIn = 0;
|
|
145
|
-
let totalOut = 0;
|
|
146
|
-
for (const log of logs) {
|
|
147
|
-
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
148
|
-
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
149
|
-
}
|
|
150
|
-
return { totalIn, totalOut };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export type SessionMembershipEvidence = {
|
|
154
|
-
groupId: string;
|
|
155
|
-
groupTitle: string;
|
|
156
|
-
groupKind: string;
|
|
157
|
-
groupStatus: string;
|
|
158
|
-
groupTask: string | null;
|
|
159
|
-
groupProject: string | null;
|
|
160
|
-
groupEvidence: InspectorGroupEvidence | null;
|
|
161
|
-
member: InspectorGroupMember;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
export type LogPaginationControls = {
|
|
165
|
-
isLoading: boolean;
|
|
166
|
-
loadProgress?: SessionLoadProgress;
|
|
167
|
-
total: number | null;
|
|
168
|
-
pageSize: number;
|
|
169
|
-
hasOlder: boolean;
|
|
170
|
-
hasNewer: boolean;
|
|
171
|
-
oldestLogId: number | null;
|
|
172
|
-
newestLogId: number | null;
|
|
173
|
-
onOldest: () => void;
|
|
174
|
-
onOlder: () => void;
|
|
175
|
-
onNewer: () => void;
|
|
176
|
-
onNewest: () => void;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
180
|
-
for (const log of logs) {
|
|
181
|
-
if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
|
|
182
|
-
return log.userAgent;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
189
|
-
const first = logs[0];
|
|
190
|
-
const last = logs[logs.length - 1];
|
|
191
|
-
if (first === undefined || last === undefined) return null;
|
|
192
|
-
return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function slateMetricToneClass(tone: SessionSlateMetricTone): string {
|
|
196
|
-
switch (tone) {
|
|
197
|
-
case "cool":
|
|
198
|
-
return "text-status-cool";
|
|
199
|
-
case "danger":
|
|
200
|
-
return "text-status-danger";
|
|
201
|
-
case "watch":
|
|
202
|
-
return "text-status-watch";
|
|
203
|
-
case "neutral":
|
|
204
|
-
return "text-foreground";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function statusMetricIcon(status: SessionSlateStatus): ReactNode {
|
|
209
|
-
switch (status) {
|
|
210
|
-
case "rolling":
|
|
211
|
-
return <RefreshCw className="size-3 text-status-cool" />;
|
|
212
|
-
case "failed":
|
|
213
|
-
return <Siren className="size-3 text-status-danger" />;
|
|
214
|
-
case "watch":
|
|
215
|
-
return <Timer className="size-3 text-status-watch" />;
|
|
216
|
-
case "clear":
|
|
217
|
-
return <Check className="size-3 text-status-success" />;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function statusMetricTone(status: SessionSlateStatus): SessionSlateMetricTone {
|
|
222
|
-
switch (status) {
|
|
223
|
-
case "rolling":
|
|
224
|
-
return "cool";
|
|
225
|
-
case "failed":
|
|
226
|
-
return "danger";
|
|
227
|
-
case "watch":
|
|
228
|
-
return "watch";
|
|
229
|
-
case "clear":
|
|
230
|
-
return "neutral";
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function requestHealthDetail(stats: SessionSlateStats): string {
|
|
235
|
-
if (stats.pending > 0) return `${stats.pending} pending / ${stats.failures} failed`;
|
|
236
|
-
if (stats.lengthStops > 0) return `${stats.lengthStops} output limit / ${stats.failures} failed`;
|
|
237
|
-
return `${stats.failures} failed / ${stats.slow} slow`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function SessionSlateMetric({
|
|
241
|
-
label,
|
|
242
|
-
value,
|
|
243
|
-
detail,
|
|
244
|
-
icon,
|
|
245
|
-
tone = "neutral",
|
|
246
|
-
}: {
|
|
247
|
-
label: string;
|
|
248
|
-
value: string;
|
|
249
|
-
detail: string;
|
|
250
|
-
icon?: ReactNode;
|
|
251
|
-
tone?: SessionSlateMetricTone;
|
|
252
|
-
}): JSX.Element {
|
|
253
|
-
return (
|
|
254
|
-
<div className="min-w-0 border-t border-border/60 px-4 py-3">
|
|
255
|
-
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
256
|
-
{icon}
|
|
257
|
-
{label}
|
|
258
|
-
</div>
|
|
259
|
-
<div className={cn("mt-2 font-mono text-sm font-semibold", slateMetricToneClass(tone))}>
|
|
260
|
-
{value}
|
|
261
|
-
</div>
|
|
262
|
-
<div className="mt-1 font-mono text-[11px] leading-snug text-muted-foreground">{detail}</div>
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
|
|
268
|
-
if (stats.requests === 0) return null;
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<div className="mb-4 overflow-hidden rounded-[8px] border border-border bg-card">
|
|
272
|
-
<div className="px-4 py-3">
|
|
273
|
-
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
274
|
-
<div className="min-w-0">
|
|
275
|
-
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
|
|
276
|
-
<Clapperboard className="size-3.5 text-slate-300" />
|
|
277
|
-
Flight Recorder
|
|
278
|
-
</div>
|
|
279
|
-
<div className="mt-2 min-w-0 font-mono text-sm font-semibold text-foreground">
|
|
280
|
-
{stats.modelLabel}
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-muted-foreground">
|
|
284
|
-
<span className="min-w-0 font-mono">{stats.providerLabel}</span>
|
|
285
|
-
<span className="min-w-0 font-mono">{stats.timeRange ?? "Timeline pending"}</span>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
<div className="grid grid-cols-2 border-t border-border/60 md:grid-cols-3 2xl:grid-cols-6">
|
|
290
|
-
<SessionSlateMetric
|
|
291
|
-
label="Status"
|
|
292
|
-
value={stats.statusLabel}
|
|
293
|
-
detail={requestHealthDetail(stats)}
|
|
294
|
-
icon={statusMetricIcon(stats.status)}
|
|
295
|
-
tone={statusMetricTone(stats.status)}
|
|
296
|
-
/>
|
|
297
|
-
<SessionSlateMetric
|
|
298
|
-
label="Turns"
|
|
299
|
-
value={String(stats.turns)}
|
|
300
|
-
detail={stats.finishLabel}
|
|
301
|
-
icon={<Flag className="size-3 text-muted-foreground" />}
|
|
302
|
-
tone={stats.finishTone}
|
|
303
|
-
/>
|
|
304
|
-
<SessionSlateMetric
|
|
305
|
-
label="Requests"
|
|
306
|
-
value={String(stats.requests)}
|
|
307
|
-
detail={`${stats.failures} failed / ${stats.slow} slow`}
|
|
308
|
-
icon={<Siren className="size-3 text-muted-foreground" />}
|
|
309
|
-
tone={stats.failures > 0 ? "danger" : "neutral"}
|
|
310
|
-
/>
|
|
311
|
-
<SessionSlateMetric
|
|
312
|
-
label="Peak"
|
|
313
|
-
value={stats.slowestLabel}
|
|
314
|
-
detail={stats.slowestDetail}
|
|
315
|
-
icon={<Timer className="size-3 text-muted-foreground" />}
|
|
316
|
-
tone={stats.slow > 0 ? "watch" : "neutral"}
|
|
317
|
-
/>
|
|
318
|
-
<SessionSlateMetric
|
|
319
|
-
label="Tokens"
|
|
320
|
-
value={stats.tokenLabel}
|
|
321
|
-
detail={stats.heaviestDetail}
|
|
322
|
-
icon={<Gauge className="size-3 text-muted-foreground" />}
|
|
323
|
-
/>
|
|
324
|
-
<SessionSlateMetric
|
|
325
|
-
label="Outcome"
|
|
326
|
-
value={stats.outcomeLabel}
|
|
327
|
-
detail={stats.outcomeDetail}
|
|
328
|
-
tone={stats.outcomeTone}
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function formatPercent(value: number | null): string {
|
|
336
|
-
if (value === null) return "Unknown";
|
|
337
|
-
const percent = value * 100;
|
|
338
|
-
if (percent >= 10) return `${percent.toFixed(0)}%`;
|
|
339
|
-
if (percent >= 1) return `${percent.toFixed(1)}%`;
|
|
340
|
-
if (percent > 0) return "<1%";
|
|
341
|
-
return "0%";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function formatTrend(value: number | null): string {
|
|
345
|
-
if (value === null) return "n/a";
|
|
346
|
-
if (Math.abs(value) < 0.1) return "0%";
|
|
347
|
-
const sign = value > 0 ? "+" : "";
|
|
348
|
-
return `${sign}${value.toFixed(1)}%`;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function progressPercent(value: number | null): number {
|
|
352
|
-
if (value === null || !Number.isFinite(value)) return 0;
|
|
353
|
-
return Math.min(Math.max(value * 100, 0), 100);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function peakMarkerPercent(
|
|
357
|
-
latestPercent: number | null,
|
|
358
|
-
peakPercent: number | null,
|
|
359
|
-
): number | null {
|
|
360
|
-
if (peakPercent === null) return null;
|
|
361
|
-
const latest = progressPercent(latestPercent);
|
|
362
|
-
const peak = progressPercent(peakPercent);
|
|
363
|
-
if (peak <= 0) return null;
|
|
364
|
-
if (Math.abs(peak - latest) < 0.5) return null;
|
|
365
|
-
return peak;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function sessionRiskBarClass(risk: ContextRiskLevel): string {
|
|
369
|
-
switch (risk) {
|
|
370
|
-
case "danger":
|
|
371
|
-
return "bg-red-400";
|
|
372
|
-
case "watch":
|
|
373
|
-
return "bg-amber-300";
|
|
374
|
-
case "unknown":
|
|
375
|
-
return "bg-muted-foreground/50";
|
|
376
|
-
case "ok":
|
|
377
|
-
return "bg-cyan-300";
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function formatContextWindow(summary: SessionModelContextSummary): string {
|
|
382
|
-
const tokens = summary.latest.contextWindow.tokens;
|
|
383
|
-
if (tokens === null) return "Context window unknown";
|
|
384
|
-
return `Context window ${formatContextWindowTokens(tokens)}`;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const MEMBER_METADATA_KEYS: string[] = [
|
|
388
|
-
"score",
|
|
389
|
-
"decision",
|
|
390
|
-
"final_status",
|
|
391
|
-
"validation_pass_ratio",
|
|
392
|
-
"semantic_memory_score",
|
|
393
|
-
"opencode_exit",
|
|
394
|
-
"actual_model",
|
|
395
|
-
"run_dir",
|
|
396
|
-
"workspace",
|
|
397
|
-
"project_dir",
|
|
398
|
-
"work_dir",
|
|
399
|
-
];
|
|
400
|
-
|
|
401
|
-
function formatMetadataValue(value: InspectorGroupMember["metadata"][string]): string | null {
|
|
402
|
-
if (typeof value === "string") return value.length > 0 ? value : null;
|
|
403
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
404
|
-
if (value === null) return null;
|
|
405
|
-
return JSON.stringify(value);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function formatMemberMetadata(member: InspectorGroupMember | null, key: string): string | null {
|
|
409
|
-
if (member === null) return null;
|
|
410
|
-
const value = member.metadata[key];
|
|
411
|
-
if (value === undefined) return null;
|
|
412
|
-
return formatMetadataValue(value);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function evidenceChipClass(value: string): string {
|
|
416
|
-
const normalized = value.toLowerCase();
|
|
417
|
-
if (
|
|
418
|
-
normalized.includes("reject") ||
|
|
419
|
-
normalized.includes("fail") ||
|
|
420
|
-
normalized.includes("error")
|
|
421
|
-
) {
|
|
422
|
-
return "border-red-400/35 bg-red-500/10 text-red-100";
|
|
423
|
-
}
|
|
424
|
-
if (
|
|
425
|
-
normalized.includes("partial") ||
|
|
426
|
-
normalized.includes("pending") ||
|
|
427
|
-
normalized.includes("slow")
|
|
428
|
-
) {
|
|
429
|
-
return "border-amber-300/35 bg-amber-400/10 text-amber-100";
|
|
430
|
-
}
|
|
431
|
-
if (
|
|
432
|
-
normalized.includes("pass") ||
|
|
433
|
-
normalized.includes("complete") ||
|
|
434
|
-
normalized.includes("accept")
|
|
435
|
-
) {
|
|
436
|
-
return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100";
|
|
437
|
-
}
|
|
438
|
-
return "border-border/70 bg-background/70 text-muted-foreground";
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function getMemberMetadataRows(member: InspectorGroupMember): { key: string; value: string }[] {
|
|
442
|
-
const rows: { key: string; value: string }[] = [];
|
|
443
|
-
for (const key of MEMBER_METADATA_KEYS) {
|
|
444
|
-
const value = member.metadata[key];
|
|
445
|
-
if (value === undefined) continue;
|
|
446
|
-
const formatted = formatMetadataValue(value);
|
|
447
|
-
if (formatted !== null) rows.push({ key, value: formatted });
|
|
448
|
-
}
|
|
449
|
-
return rows;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function SessionMembershipPanel({
|
|
453
|
-
memberships,
|
|
454
|
-
defaultOpen = false,
|
|
455
|
-
}: {
|
|
456
|
-
memberships: SessionMembershipEvidence[];
|
|
457
|
-
defaultOpen?: boolean;
|
|
458
|
-
}): JSX.Element | null {
|
|
459
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
460
|
-
if (memberships.length === 0) return null;
|
|
461
|
-
const primary = memberships[0];
|
|
462
|
-
const primaryMember = primary?.member ?? null;
|
|
463
|
-
const score = formatMemberMetadata(primaryMember, "score");
|
|
464
|
-
const decision = formatMemberMetadata(primaryMember, "decision");
|
|
465
|
-
const finalStatus = formatMemberMetadata(primaryMember, "final_status");
|
|
466
|
-
const summaryChips = [
|
|
467
|
-
score === null ? null : { label: `score ${score}`, value: score },
|
|
468
|
-
decision === null ? null : { label: decision, value: decision },
|
|
469
|
-
finalStatus === null ? null : { label: finalStatus, value: finalStatus },
|
|
470
|
-
primaryMember?.model === null || primaryMember?.model === undefined
|
|
471
|
-
? null
|
|
472
|
-
: { label: primaryMember.model, value: primaryMember.model },
|
|
473
|
-
].filter(
|
|
474
|
-
(value): value is { label: string; value: string } =>
|
|
475
|
-
value !== null && value.label.length > 0 && value.value.length > 0,
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
return (
|
|
479
|
-
<div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] text-xs">
|
|
480
|
-
<button
|
|
481
|
-
type="button"
|
|
482
|
-
onClick={() => setOpen((value) => !value)}
|
|
483
|
-
className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-white/[0.04]"
|
|
484
|
-
aria-expanded={open}
|
|
485
|
-
>
|
|
486
|
-
<div className="min-w-0 flex-1">
|
|
487
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
488
|
-
<span className="font-semibold text-foreground">Case File</span>
|
|
489
|
-
{primary !== undefined && (
|
|
490
|
-
<span className="truncate text-muted-foreground" title={primary.groupTitle}>
|
|
491
|
-
{primary.groupTitle}
|
|
492
|
-
</span>
|
|
493
|
-
)}
|
|
494
|
-
<span className="font-mono text-[10px] uppercase text-muted-foreground">
|
|
495
|
-
{memberships.length} group{memberships.length === 1 ? "" : "s"}
|
|
496
|
-
</span>
|
|
497
|
-
</div>
|
|
498
|
-
{summaryChips.length > 0 && (
|
|
499
|
-
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
500
|
-
{summaryChips.slice(0, 4).map((chip) => (
|
|
501
|
-
<span
|
|
502
|
-
key={chip.label}
|
|
503
|
-
className={cn(
|
|
504
|
-
"rounded border px-1.5 py-0.5 font-mono text-[10px]",
|
|
505
|
-
evidenceChipClass(chip.value),
|
|
506
|
-
)}
|
|
507
|
-
>
|
|
508
|
-
{chip.label}
|
|
509
|
-
</span>
|
|
510
|
-
))}
|
|
511
|
-
</div>
|
|
512
|
-
)}
|
|
513
|
-
</div>
|
|
514
|
-
{open ? (
|
|
515
|
-
<ChevronUp className="size-4 shrink-0 text-muted-foreground" />
|
|
516
|
-
) : (
|
|
517
|
-
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
|
|
518
|
-
)}
|
|
519
|
-
</button>
|
|
520
|
-
{open && (
|
|
521
|
-
<div className="space-y-2 border-t border-white/10 px-3 py-2">
|
|
522
|
-
{memberships.map((membership) => {
|
|
523
|
-
const member = membership.member;
|
|
524
|
-
const metadataRows = getMemberMetadataRows(member);
|
|
525
|
-
return (
|
|
526
|
-
<div key={`${membership.groupId}:${member.id}`} className="min-w-0">
|
|
527
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
528
|
-
<span
|
|
529
|
-
className="truncate font-medium text-foreground"
|
|
530
|
-
title={membership.groupTitle}
|
|
531
|
-
>
|
|
532
|
-
{membership.groupTitle}
|
|
533
|
-
</span>
|
|
534
|
-
<span className="font-mono text-[10px] uppercase text-muted-foreground">
|
|
535
|
-
{membership.groupStatus}
|
|
536
|
-
</span>
|
|
537
|
-
{member.status !== null && (
|
|
538
|
-
<span className="font-mono text-[10px] text-muted-foreground">
|
|
539
|
-
member {member.status}
|
|
540
|
-
</span>
|
|
541
|
-
)}
|
|
542
|
-
</div>
|
|
543
|
-
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
|
|
544
|
-
<span className="font-mono" title={member.sessionId}>
|
|
545
|
-
{truncateSessionId(member.sessionId)}
|
|
546
|
-
</span>
|
|
547
|
-
{member.label !== null && <span>{member.label}</span>}
|
|
548
|
-
{member.provider !== null && <span>{member.provider}</span>}
|
|
549
|
-
{member.model !== null && <span className="font-mono">{member.model}</span>}
|
|
550
|
-
{member.runId !== null && (
|
|
551
|
-
<span className="font-mono" title={member.runId}>
|
|
552
|
-
run {truncateSessionId(member.runId)}
|
|
553
|
-
</span>
|
|
554
|
-
)}
|
|
555
|
-
</div>
|
|
556
|
-
{metadataRows.length > 0 && (
|
|
557
|
-
<div className="mt-2 grid gap-1 sm:grid-cols-2 xl:grid-cols-3">
|
|
558
|
-
{metadataRows.map((row) => (
|
|
559
|
-
<div
|
|
560
|
-
key={row.key}
|
|
561
|
-
className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-1"
|
|
562
|
-
>
|
|
563
|
-
<div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
|
|
564
|
-
<div
|
|
565
|
-
className="truncate font-mono text-[11px] text-foreground"
|
|
566
|
-
title={row.value}
|
|
567
|
-
>
|
|
568
|
-
{row.value}
|
|
569
|
-
</div>
|
|
570
|
-
</div>
|
|
571
|
-
))}
|
|
572
|
-
</div>
|
|
573
|
-
)}
|
|
574
|
-
</div>
|
|
575
|
-
);
|
|
576
|
-
})}
|
|
577
|
-
</div>
|
|
578
|
-
)}
|
|
579
|
-
</div>
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function LogPaginationButton({
|
|
584
|
-
label,
|
|
585
|
-
title,
|
|
586
|
-
icon,
|
|
587
|
-
disabled,
|
|
588
|
-
onClick,
|
|
589
|
-
}: {
|
|
590
|
-
label: string;
|
|
591
|
-
title: string;
|
|
592
|
-
icon: JSX.Element;
|
|
593
|
-
disabled: boolean;
|
|
594
|
-
onClick: () => void;
|
|
595
|
-
}): JSX.Element {
|
|
596
|
-
return (
|
|
597
|
-
<button
|
|
598
|
-
type="button"
|
|
599
|
-
onClick={onClick}
|
|
600
|
-
disabled={disabled}
|
|
601
|
-
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45"
|
|
602
|
-
title={title}
|
|
603
|
-
>
|
|
604
|
-
{icon}
|
|
605
|
-
<span>{label}</span>
|
|
606
|
-
</button>
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function LogPaginationBar({
|
|
611
|
-
logs,
|
|
612
|
-
pagination,
|
|
613
|
-
embedded = false,
|
|
614
|
-
}: {
|
|
615
|
-
logs: readonly CapturedLog[];
|
|
616
|
-
pagination: LogPaginationControls | undefined;
|
|
617
|
-
embedded?: boolean;
|
|
618
|
-
}): JSX.Element | null {
|
|
619
|
-
if (pagination === undefined) return null;
|
|
620
|
-
const disabled = pagination.isLoading;
|
|
621
|
-
|
|
622
|
-
return (
|
|
623
|
-
<nav
|
|
624
|
-
aria-label="Session log pagination"
|
|
625
|
-
className={cn(
|
|
626
|
-
"flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
|
|
627
|
-
embedded
|
|
628
|
-
? "border-t border-white/10 pt-2"
|
|
629
|
-
: "border border-border bg-card shadow-sm mb-3 rounded-[8px] px-2",
|
|
630
|
-
)}
|
|
631
|
-
>
|
|
632
|
-
<div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
|
|
633
|
-
<div role="status" aria-live="polite" aria-atomic="true">
|
|
634
|
-
{formatPaginationStatus({ logs, pagination })}
|
|
635
|
-
{pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
|
|
636
|
-
</div>
|
|
637
|
-
{pagination.loadProgress !== undefined && (
|
|
638
|
-
<SessionLoadProgressBar progress={pagination.loadProgress} />
|
|
639
|
-
)}
|
|
640
|
-
</div>
|
|
641
|
-
<div className="flex w-full items-center gap-1 overflow-x-auto sm:w-auto">
|
|
642
|
-
<LogPaginationButton
|
|
643
|
-
label="Oldest"
|
|
644
|
-
title="Load the oldest logs in this session"
|
|
645
|
-
icon={<ChevronsLeft className="size-3.5" />}
|
|
646
|
-
disabled={disabled || !pagination.hasOlder}
|
|
647
|
-
onClick={pagination.onOldest}
|
|
648
|
-
/>
|
|
649
|
-
<LogPaginationButton
|
|
650
|
-
label="Older"
|
|
651
|
-
title="Load older logs"
|
|
652
|
-
icon={<ChevronLeft className="size-3.5" />}
|
|
653
|
-
disabled={disabled || !pagination.hasOlder}
|
|
654
|
-
onClick={pagination.onOlder}
|
|
655
|
-
/>
|
|
656
|
-
<LogPaginationButton
|
|
657
|
-
label="Newer"
|
|
658
|
-
title="Load newer logs"
|
|
659
|
-
icon={<ChevronRight className="size-3.5" />}
|
|
660
|
-
disabled={disabled || !pagination.hasNewer}
|
|
661
|
-
onClick={pagination.onNewer}
|
|
662
|
-
/>
|
|
663
|
-
<LogPaginationButton
|
|
664
|
-
label="Newest"
|
|
665
|
-
title="Load the newest logs in this session"
|
|
666
|
-
icon={<ChevronsRight className="size-3.5" />}
|
|
667
|
-
disabled={disabled || !pagination.hasNewer}
|
|
668
|
-
onClick={pagination.onNewest}
|
|
669
|
-
/>
|
|
670
|
-
</div>
|
|
671
|
-
</nav>
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function formatRequestCount(count: number): string {
|
|
676
|
-
return `${count} ${count === 1 ? "request" : "requests"}`;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function formatLogReference(logId: number): string {
|
|
680
|
-
return `#${String(logId)}`;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function sessionRiskClass(risk: ContextRiskLevel): string {
|
|
684
|
-
switch (risk) {
|
|
685
|
-
case "danger":
|
|
686
|
-
return "text-red-300";
|
|
687
|
-
case "watch":
|
|
688
|
-
return "text-amber-200";
|
|
689
|
-
case "unknown":
|
|
690
|
-
return "text-muted-foreground";
|
|
691
|
-
case "ok":
|
|
692
|
-
return "text-cyan-200";
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function sessionHealthClass(level: ContextHealthLevel): string {
|
|
697
|
-
switch (level) {
|
|
698
|
-
case "risk":
|
|
699
|
-
return "border-red-400/30 bg-red-500/10 text-red-100";
|
|
700
|
-
case "optimizable":
|
|
701
|
-
return "border-amber-400/25 bg-amber-400/8 text-amber-100";
|
|
702
|
-
case "ok":
|
|
703
|
-
return "border-cyan-300/25 bg-cyan-400/8 text-cyan-100";
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function sessionHealthLabel(level: ContextHealthLevel): string {
|
|
708
|
-
switch (level) {
|
|
709
|
-
case "risk":
|
|
710
|
-
return "Risk";
|
|
711
|
-
case "optimizable":
|
|
712
|
-
return "Optimizable";
|
|
713
|
-
case "ok":
|
|
714
|
-
return "OK";
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const LOG_FOCUS_RETRY_LIMIT = 10;
|
|
719
|
-
const LOG_FOCUS_RETRY_DELAY_MS = 80;
|
|
720
|
-
|
|
721
|
-
function highlightLogTarget(target: HTMLElement): void {
|
|
722
|
-
target.setAttribute("data-deep-link-highlight", "true");
|
|
723
|
-
window.setTimeout(() => {
|
|
724
|
-
target.removeAttribute("data-deep-link-highlight");
|
|
725
|
-
}, 1800);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function focusLogTarget(logId: number): boolean {
|
|
729
|
-
const target = document.getElementById(`log-${String(logId)}`);
|
|
730
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
731
|
-
target.scrollIntoView({
|
|
732
|
-
block: "center",
|
|
733
|
-
behavior: preferredScrollBehavior(browserPrefersReducedMotion()),
|
|
734
|
-
});
|
|
735
|
-
target.focus({ preventScroll: true });
|
|
736
|
-
highlightLogTarget(target);
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function openLogContext(logId: number): void {
|
|
741
|
-
dispatchLogFocusRequest({ logId, tab: "anatomy" });
|
|
742
|
-
|
|
743
|
-
let attempts = 0;
|
|
744
|
-
const tryFocus = (): void => {
|
|
745
|
-
dispatchLogFocusRequest({ logId, tab: "anatomy" });
|
|
746
|
-
if (focusLogTarget(logId)) return;
|
|
747
|
-
|
|
748
|
-
attempts += 1;
|
|
749
|
-
if (attempts < LOG_FOCUS_RETRY_LIMIT) {
|
|
750
|
-
window.setTimeout(tryFocus, LOG_FOCUS_RETRY_DELAY_MS);
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
window.setTimeout(tryFocus, 0);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
758
|
-
const [copied, setCopied] = useState(false);
|
|
759
|
-
|
|
760
|
-
const handleCopy = useCallback(() => {
|
|
761
|
-
void copyTextToClipboard(command).then((success) => {
|
|
762
|
-
if (!success) return;
|
|
763
|
-
setCopied(true);
|
|
764
|
-
setTimeout(() => setCopied(false), 2000);
|
|
765
|
-
});
|
|
766
|
-
}, [command]);
|
|
767
|
-
|
|
768
|
-
return (
|
|
769
|
-
<div data-command-example="true" className={COPYABLE_COMMAND_CONTAINER_CLASS_NAME}>
|
|
770
|
-
<pre className={COPYABLE_COMMAND_TEXT_CLASS_NAME}>{command}</pre>
|
|
771
|
-
<button
|
|
772
|
-
type="button"
|
|
773
|
-
onClick={handleCopy}
|
|
774
|
-
className="shrink-0 cursor-pointer rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
775
|
-
aria-label="Copy command"
|
|
776
|
-
>
|
|
777
|
-
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
778
|
-
</button>
|
|
779
|
-
</div>
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function SidebarPanel({
|
|
784
|
-
label,
|
|
785
|
-
children,
|
|
786
|
-
className,
|
|
787
|
-
}: {
|
|
788
|
-
label: string;
|
|
789
|
-
children: ReactNode;
|
|
790
|
-
className?: string;
|
|
791
|
-
}): JSX.Element {
|
|
792
|
-
return (
|
|
793
|
-
<section className={cn("rounded-[8px] border border-border/60 bg-card/55", className)}>
|
|
794
|
-
<div className="px-3 pt-3">
|
|
795
|
-
<div className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-text-subtle">
|
|
796
|
-
{label}
|
|
797
|
-
</div>
|
|
798
|
-
</div>
|
|
799
|
-
<div className="p-3 pt-2">{children}</div>
|
|
800
|
-
</section>
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function SidebarMetric({ label, value }: { label: string; value: string | number }): JSX.Element {
|
|
805
|
-
return (
|
|
806
|
-
<div className="min-w-0 rounded-md border border-border/50 bg-muted/30 px-2 py-2">
|
|
807
|
-
<div className="truncate font-mono text-lg font-semibold leading-6 text-text-strong">
|
|
808
|
-
{value}
|
|
809
|
-
</div>
|
|
810
|
-
<div className="mt-0.5 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-text-subtle">
|
|
811
|
-
{label}
|
|
812
|
-
</div>
|
|
813
|
-
</div>
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function ThemeModeToggle(): JSX.Element {
|
|
818
|
-
const { themeMode, setThemeMode } = useThemeMode();
|
|
819
|
-
|
|
820
|
-
return (
|
|
821
|
-
<div
|
|
822
|
-
className="inline-flex h-9 items-center rounded-md border border-border/70 bg-muted/70 p-0.5"
|
|
823
|
-
aria-label="Theme mode"
|
|
824
|
-
role="group"
|
|
825
|
-
>
|
|
826
|
-
<button
|
|
827
|
-
type="button"
|
|
828
|
-
onClick={() => setThemeMode("light")}
|
|
829
|
-
className={cn(
|
|
830
|
-
"inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
|
|
831
|
-
themeMode === "light" && "bg-background text-foreground shadow-sm",
|
|
832
|
-
)}
|
|
833
|
-
aria-label="Use light mode"
|
|
834
|
-
aria-pressed={themeMode === "light"}
|
|
835
|
-
title="Light mode"
|
|
836
|
-
>
|
|
837
|
-
<Sun className="size-3.5" />
|
|
838
|
-
</button>
|
|
839
|
-
<button
|
|
840
|
-
type="button"
|
|
841
|
-
onClick={() => setThemeMode("eye-care")}
|
|
842
|
-
className={cn(
|
|
843
|
-
"inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
|
|
844
|
-
themeMode === "eye-care" && "bg-background text-foreground shadow-sm",
|
|
845
|
-
)}
|
|
846
|
-
aria-label="Use eye care mode"
|
|
847
|
-
aria-pressed={themeMode === "eye-care"}
|
|
848
|
-
title="Eye care mode"
|
|
849
|
-
>
|
|
850
|
-
<Eye className="size-3.5" />
|
|
851
|
-
</button>
|
|
852
|
-
<button
|
|
853
|
-
type="button"
|
|
854
|
-
onClick={() => setThemeMode("dark")}
|
|
855
|
-
className={cn(
|
|
856
|
-
"inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
|
|
857
|
-
themeMode === "dark" && "bg-background text-foreground shadow-sm",
|
|
858
|
-
)}
|
|
859
|
-
aria-label="Use dark mode"
|
|
860
|
-
aria-pressed={themeMode === "dark"}
|
|
861
|
-
title="Dark mode"
|
|
862
|
-
>
|
|
863
|
-
<Moon className="size-3.5" />
|
|
864
|
-
</button>
|
|
865
|
-
</div>
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function PetVisibilityToggle({
|
|
870
|
-
visible,
|
|
871
|
-
onToggle,
|
|
872
|
-
}: {
|
|
873
|
-
visible: boolean;
|
|
874
|
-
onToggle: () => void;
|
|
875
|
-
}): JSX.Element {
|
|
876
|
-
const label = visible ? "Hide Inspector pet" : "Summon Inspector pet";
|
|
877
|
-
|
|
878
|
-
return (
|
|
879
|
-
<button
|
|
880
|
-
type="button"
|
|
881
|
-
onClick={onToggle}
|
|
882
|
-
className={cn(
|
|
883
|
-
"border-border/70 bg-muted/70 inline-flex size-8 shrink-0 items-center justify-center rounded-md border text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
|
|
884
|
-
visible && "bg-background text-amber-500 shadow-sm",
|
|
885
|
-
)}
|
|
886
|
-
aria-label={label}
|
|
887
|
-
aria-pressed={visible}
|
|
888
|
-
title={label}
|
|
889
|
-
>
|
|
890
|
-
<CrabLogo className={cn("size-4 transition-opacity", !visible && "opacity-45")} />
|
|
891
|
-
</button>
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function UiScaleControl({
|
|
896
|
-
preference,
|
|
897
|
-
resolvedScale,
|
|
898
|
-
onChange,
|
|
899
|
-
}: {
|
|
900
|
-
preference: UiScalePreference;
|
|
901
|
-
resolvedScale: number;
|
|
902
|
-
onChange: (preference: UiScalePreference) => void;
|
|
903
|
-
}): JSX.Element {
|
|
904
|
-
const resolvedLabel = formatUiScale(resolvedScale);
|
|
905
|
-
const currentLabel = preference === "auto" ? `Auto (${resolvedLabel})` : `${preference}%`;
|
|
906
|
-
|
|
907
|
-
return (
|
|
908
|
-
<Select
|
|
909
|
-
value={preference}
|
|
910
|
-
onValueChange={(value) => onChange(normalizeUiScalePreference(value))}
|
|
911
|
-
>
|
|
912
|
-
<SelectTrigger
|
|
913
|
-
size="sm"
|
|
914
|
-
className="h-9 min-w-[4.5rem] justify-center gap-1 border-border/70 bg-muted/70 px-2 py-0 text-muted-foreground hover:bg-muted max-lg:h-11 [&>svg:last-child]:hidden"
|
|
915
|
-
aria-label={`Interface size: ${currentLabel}`}
|
|
916
|
-
>
|
|
917
|
-
<ALargeSmall className="size-3.5" />
|
|
918
|
-
<span className="font-mono text-[10px] font-semibold tabular-nums">{resolvedLabel}</span>
|
|
919
|
-
</SelectTrigger>
|
|
920
|
-
<SelectContent align="end">
|
|
921
|
-
<SelectItem value="auto">Auto</SelectItem>
|
|
922
|
-
<SelectItem value="100">100%</SelectItem>
|
|
923
|
-
<SelectItem value="125">125%</SelectItem>
|
|
924
|
-
<SelectItem value="150">150%</SelectItem>
|
|
925
|
-
</SelectContent>
|
|
926
|
-
</Select>
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
|
|
931
|
-
const DETAILS_PANEL_WIDTH_STORAGE_KEY = "agent-inspector.detailsPanelWidth";
|
|
932
|
-
const INSPECTOR_PET_VISIBLE_STORAGE_KEY = "agent-inspector.pet.visible";
|
|
933
|
-
const DETAILS_PANEL_DEFAULT_WIDTH = 360;
|
|
934
|
-
const DETAILS_PANEL_MIN_WIDTH = 288;
|
|
935
|
-
const DETAILS_PANEL_MAX_WIDTH = 520;
|
|
936
|
-
|
|
937
|
-
type DetailsPanelResizeState = {
|
|
938
|
-
startX: number;
|
|
939
|
-
startWidth: number;
|
|
940
|
-
pointerId: number;
|
|
941
|
-
};
|
|
942
|
-
|
|
943
|
-
function clampDetailsPanelWidth(width: number): number {
|
|
944
|
-
return Math.min(Math.max(width, DETAILS_PANEL_MIN_WIDTH), DETAILS_PANEL_MAX_WIDTH);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function readStoredDetailsPanelCollapsed(): boolean {
|
|
948
|
-
if (typeof window === "undefined") return true;
|
|
949
|
-
try {
|
|
950
|
-
const stored = window.localStorage.getItem(DETAILS_PANEL_COLLAPSED_STORAGE_KEY);
|
|
951
|
-
return stored === null || stored === "1";
|
|
952
|
-
} catch {
|
|
953
|
-
return true;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
function readStoredDetailsPanelWidth(): number {
|
|
958
|
-
if (typeof window === "undefined") return DETAILS_PANEL_DEFAULT_WIDTH;
|
|
959
|
-
try {
|
|
960
|
-
const stored = window.localStorage.getItem(DETAILS_PANEL_WIDTH_STORAGE_KEY);
|
|
961
|
-
if (stored === null) return DETAILS_PANEL_DEFAULT_WIDTH;
|
|
962
|
-
const parsed = Number.parseInt(stored, 10);
|
|
963
|
-
return Number.isFinite(parsed) ? clampDetailsPanelWidth(parsed) : DETAILS_PANEL_DEFAULT_WIDTH;
|
|
964
|
-
} catch {
|
|
965
|
-
return DETAILS_PANEL_DEFAULT_WIDTH;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function readStoredInspectorPetVisible(): boolean {
|
|
970
|
-
if (typeof window === "undefined") return true;
|
|
971
|
-
try {
|
|
972
|
-
return window.localStorage.getItem(INSPECTOR_PET_VISIBLE_STORAGE_KEY) !== "0";
|
|
973
|
-
} catch {
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
|
|
979
|
-
return (
|
|
980
|
-
<button
|
|
981
|
-
type="button"
|
|
982
|
-
onClick={() => openLogContext(logId)}
|
|
983
|
-
className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
|
|
984
|
-
title={`Open Context for ${formatLogReference(logId)}`}
|
|
985
|
-
>
|
|
986
|
-
{label} {formatLogReference(logId)}
|
|
987
|
-
</button>
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function SessionContextProgressBar({
|
|
992
|
-
latestPercent,
|
|
993
|
-
peakPercent,
|
|
994
|
-
risk,
|
|
995
|
-
onOpenContext,
|
|
996
|
-
}: {
|
|
997
|
-
latestPercent: number | null;
|
|
998
|
-
peakPercent: number | null;
|
|
999
|
-
risk: ContextRiskLevel;
|
|
1000
|
-
onOpenContext: () => void;
|
|
1001
|
-
}): JSX.Element {
|
|
1002
|
-
const latest = progressPercent(latestPercent);
|
|
1003
|
-
const peakMarker = peakMarkerPercent(latestPercent, peakPercent);
|
|
1004
|
-
const ariaLabel =
|
|
1005
|
-
peakPercent === null
|
|
1006
|
-
? `Latest context window usage ${formatPercent(latestPercent)}`
|
|
1007
|
-
: `Latest context window usage ${formatPercent(latestPercent)}, peak ${formatPercent(
|
|
1008
|
-
peakPercent,
|
|
1009
|
-
)}`;
|
|
1010
|
-
|
|
1011
|
-
return (
|
|
1012
|
-
<button
|
|
1013
|
-
type="button"
|
|
1014
|
-
onClick={onOpenContext}
|
|
1015
|
-
className="group block w-full rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
1016
|
-
aria-label={`${ariaLabel}. Open latest Context details`}
|
|
1017
|
-
title="Open latest Context details"
|
|
1018
|
-
>
|
|
1019
|
-
<span
|
|
1020
|
-
className="relative block h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/25 transition-colors group-hover:border-cyan-200/35"
|
|
1021
|
-
role="meter"
|
|
1022
|
-
aria-label={ariaLabel}
|
|
1023
|
-
aria-valuemin={0}
|
|
1024
|
-
aria-valuemax={100}
|
|
1025
|
-
aria-valuenow={Math.round(latest)}
|
|
1026
|
-
>
|
|
1027
|
-
<span
|
|
1028
|
-
className={cn("block h-full rounded-full transition-[width]", sessionRiskBarClass(risk))}
|
|
1029
|
-
style={{ width: `${latest}%` }}
|
|
1030
|
-
/>
|
|
1031
|
-
{peakMarker !== null && (
|
|
1032
|
-
<span
|
|
1033
|
-
className="absolute top-0 h-full w-0.5 rounded-full bg-amber-100/75 shadow-[0_0_0_1px_rgba(0,0,0,0.25)]"
|
|
1034
|
-
style={{ left: `calc(${peakMarker}% - 0.5px)` }}
|
|
1035
|
-
aria-hidden="true"
|
|
1036
|
-
/>
|
|
1037
|
-
)}
|
|
1038
|
-
</span>
|
|
1039
|
-
</button>
|
|
1040
|
-
);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function SessionModelContextRow({ summary }: { summary: SessionModelContextSummary }): JSX.Element {
|
|
1044
|
-
const trend = summary.trendPercent;
|
|
1045
|
-
const TrendIcon =
|
|
1046
|
-
trend === null || Math.abs(trend) < 0.1 ? null : trend > 0 ? ArrowUpRight : ArrowDownRight;
|
|
1047
|
-
const diagnosticLabel = summary.latest.diagnostics[0] ?? null;
|
|
1048
|
-
const latestUsage = formatPercent(summary.latest.usagePercent);
|
|
1049
|
-
const peakUsage = formatPercent(summary.peak.usagePercent);
|
|
1050
|
-
const openLatestContext = (): void => {
|
|
1051
|
-
openLogContext(summary.latest.logId);
|
|
1052
|
-
};
|
|
1053
|
-
|
|
1054
|
-
return (
|
|
1055
|
-
<div className="grid gap-x-4 gap-y-2 border-t border-white/10 py-2 first:border-t-0 lg:grid-cols-[minmax(180px,1fr)_minmax(260px,1fr)_minmax(260px,1.4fr)]">
|
|
1056
|
-
<div className="min-w-0">
|
|
1057
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
1058
|
-
<button
|
|
1059
|
-
type="button"
|
|
1060
|
-
onClick={openLatestContext}
|
|
1061
|
-
className="min-w-0 truncate rounded-sm text-left font-mono font-semibold text-foreground transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
1062
|
-
title={`Open latest Context for ${summary.model}`}
|
|
1063
|
-
>
|
|
1064
|
-
{summary.model}
|
|
1065
|
-
</button>
|
|
1066
|
-
<span
|
|
1067
|
-
className={cn(
|
|
1068
|
-
"inline-flex h-5 shrink-0 items-center rounded border px-1.5 font-mono text-[10px]",
|
|
1069
|
-
sessionHealthClass(summary.latest.health.level),
|
|
1070
|
-
)}
|
|
1071
|
-
>
|
|
1072
|
-
{sessionHealthLabel(summary.latest.health.level)}
|
|
1073
|
-
</span>
|
|
1074
|
-
</div>
|
|
1075
|
-
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
|
|
1076
|
-
<span>{formatRequestCount(summary.requestCount)}</span>
|
|
1077
|
-
<span>{formatContextWindow(summary)}</span>
|
|
1078
|
-
</div>
|
|
1079
|
-
</div>
|
|
1080
|
-
|
|
1081
|
-
<div className="min-w-0 space-y-2 text-xs">
|
|
1082
|
-
<SessionContextProgressBar
|
|
1083
|
-
latestPercent={summary.latest.usagePercent}
|
|
1084
|
-
peakPercent={summary.peak.usagePercent}
|
|
1085
|
-
risk={summary.latest.riskLevel}
|
|
1086
|
-
onOpenContext={openLatestContext}
|
|
1087
|
-
/>
|
|
1088
|
-
<div className="grid grid-cols-3 gap-2">
|
|
1089
|
-
<div className="min-w-0">
|
|
1090
|
-
<div className="text-[10px] uppercase text-muted-foreground">Latest</div>
|
|
1091
|
-
<div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.latest.riskLevel))}>
|
|
1092
|
-
{latestUsage}
|
|
1093
|
-
</div>
|
|
1094
|
-
<SessionContextLogButton label="Log" logId={summary.latest.logId} />
|
|
1095
|
-
</div>
|
|
1096
|
-
<div className="min-w-0">
|
|
1097
|
-
<div className="text-[10px] uppercase text-muted-foreground">Peak</div>
|
|
1098
|
-
<div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.peak.riskLevel))}>
|
|
1099
|
-
{peakUsage}
|
|
1100
|
-
</div>
|
|
1101
|
-
<SessionContextLogButton label="Log" logId={summary.peak.logId} />
|
|
1102
|
-
</div>
|
|
1103
|
-
<div className="min-w-0">
|
|
1104
|
-
<div className="text-[10px] uppercase text-muted-foreground">Trend</div>
|
|
1105
|
-
<div className="mt-0.5 flex items-center gap-1 font-mono text-foreground">
|
|
1106
|
-
{TrendIcon !== null && (
|
|
1107
|
-
<TrendIcon
|
|
1108
|
-
className={cn(
|
|
1109
|
-
"size-3",
|
|
1110
|
-
trend !== null && trend > 0 ? "text-amber-300" : "text-cyan-300",
|
|
1111
|
-
)}
|
|
1112
|
-
/>
|
|
1113
|
-
)}
|
|
1114
|
-
{formatTrend(trend)}
|
|
1115
|
-
</div>
|
|
1116
|
-
<div className="h-5" aria-hidden="true" />
|
|
1117
|
-
</div>
|
|
1118
|
-
</div>
|
|
1119
|
-
</div>
|
|
1120
|
-
|
|
1121
|
-
<div className="min-w-0 space-y-1.5">
|
|
1122
|
-
<SegmentBar
|
|
1123
|
-
segments={summary.latest.roleSegments}
|
|
1124
|
-
totalTokens={summary.latest.roleTotalTokens}
|
|
1125
|
-
showLabels={false}
|
|
1126
|
-
/>
|
|
1127
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
|
|
1128
|
-
<span className="font-mono">
|
|
1129
|
-
~{formatTokens(summary.latest.estimatedInputTokens)} input
|
|
1130
|
-
</span>
|
|
1131
|
-
{summary.latest.windowUsedTokens !== null && (
|
|
1132
|
-
<span className="font-mono">~{formatTokens(summary.latest.windowUsedTokens)} used</span>
|
|
1133
|
-
)}
|
|
1134
|
-
{diagnosticLabel !== null && <span className="truncate">{diagnosticLabel}</span>}
|
|
1135
|
-
</div>
|
|
1136
|
-
</div>
|
|
1137
|
-
</div>
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function SessionContextSummaryPanel({
|
|
1142
|
-
summary,
|
|
1143
|
-
}: {
|
|
1144
|
-
summary: SessionContextSummary;
|
|
1145
|
-
}): JSX.Element | null {
|
|
1146
|
-
if (summary.models.length === 0) return null;
|
|
1147
|
-
return (
|
|
1148
|
-
<div className="mt-3 border-t border-white/10 pt-2">
|
|
1149
|
-
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1150
|
-
<div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
|
|
1151
|
-
Session Context
|
|
1152
|
-
</div>
|
|
1153
|
-
<div className="font-mono text-[10px] text-muted-foreground">
|
|
1154
|
-
{summary.models.length} model{summary.models.length === 1 ? "" : "s"}
|
|
1155
|
-
</div>
|
|
1156
|
-
</div>
|
|
1157
|
-
<div className="max-h-52 overflow-y-auto pr-1">
|
|
1158
|
-
{summary.models.map((modelSummary) => (
|
|
1159
|
-
<SessionModelContextRow key={modelSummary.model} summary={modelSummary} />
|
|
1160
|
-
))}
|
|
1161
|
-
</div>
|
|
1162
|
-
</div>
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function formatSessionContextOverview(summary: SessionContextSummary): string {
|
|
1167
|
-
const riskCount = summary.models.filter((model) => model.latest.health.level === "risk").length;
|
|
1168
|
-
if (riskCount > 0) return `${riskCount} risk`;
|
|
1169
|
-
|
|
1170
|
-
const optimizableCount = summary.models.filter(
|
|
1171
|
-
(model) => model.latest.health.level === "optimizable",
|
|
1172
|
-
).length;
|
|
1173
|
-
if (optimizableCount > 0) return `${optimizableCount} optimizable`;
|
|
1174
|
-
|
|
1175
|
-
return `${summary.models.length} model${summary.models.length === 1 ? "" : "s"}`;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function SessionContextBar({
|
|
1179
|
-
label,
|
|
1180
|
-
logs,
|
|
1181
|
-
totalIn,
|
|
1182
|
-
totalOut,
|
|
1183
|
-
timeDisplayFormat,
|
|
1184
|
-
contextSummary,
|
|
1185
|
-
showBackLink,
|
|
1186
|
-
}: {
|
|
1187
|
-
label: string;
|
|
1188
|
-
logs: CapturedLog[];
|
|
1189
|
-
totalIn: number;
|
|
1190
|
-
totalOut: number;
|
|
1191
|
-
timeDisplayFormat: TimeDisplayFormat;
|
|
1192
|
-
contextSummary: SessionContextSummary;
|
|
1193
|
-
showBackLink: boolean;
|
|
1194
|
-
}): JSX.Element {
|
|
1195
|
-
const [copied, setCopied] = useState(false);
|
|
1196
|
-
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
1197
|
-
const timeRange = useMemo(
|
|
1198
|
-
() => formatTimeRange(logs, timeDisplayFormat),
|
|
1199
|
-
[logs, timeDisplayFormat],
|
|
1200
|
-
);
|
|
1201
|
-
const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
|
|
1202
|
-
const contextOverview = useMemo(
|
|
1203
|
-
() => formatSessionContextOverview(contextSummary),
|
|
1204
|
-
[contextSummary],
|
|
1205
|
-
);
|
|
1206
|
-
|
|
1207
|
-
const handleCopyLink = useCallback(() => {
|
|
1208
|
-
void copyTextToClipboard(window.location.href).then((success) => {
|
|
1209
|
-
if (!success) return;
|
|
1210
|
-
setCopied(true);
|
|
1211
|
-
setTimeout(() => setCopied(false), 2000);
|
|
1212
|
-
});
|
|
1213
|
-
}, []);
|
|
1214
|
-
|
|
1215
|
-
return (
|
|
1216
|
-
<div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] px-3 py-2 text-xs">
|
|
1217
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1218
|
-
{showBackLink && (
|
|
1219
|
-
<a
|
|
1220
|
-
href={withBasePath("/")}
|
|
1221
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
|
|
1222
|
-
aria-label="Back to all sessions"
|
|
1223
|
-
title="Back to all sessions"
|
|
1224
|
-
>
|
|
1225
|
-
<ArrowLeft className="size-3.5" />
|
|
1226
|
-
</a>
|
|
1227
|
-
)}
|
|
1228
|
-
<div className="min-w-[180px] flex-1">
|
|
1229
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
1230
|
-
<span className="font-mono font-semibold text-purple-400/90 truncate" title={label}>
|
|
1231
|
-
{truncateSessionId(label)}
|
|
1232
|
-
</span>
|
|
1233
|
-
{userAgent !== null && (
|
|
1234
|
-
<span
|
|
1235
|
-
className="font-mono text-muted-foreground truncate max-w-[220px]"
|
|
1236
|
-
title={userAgent}
|
|
1237
|
-
>
|
|
1238
|
-
{userAgent}
|
|
1239
|
-
</span>
|
|
1240
|
-
)}
|
|
1241
|
-
</div>
|
|
1242
|
-
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
|
|
1243
|
-
<span>
|
|
1244
|
-
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
1245
|
-
</span>
|
|
1246
|
-
{timeRange !== null && <span>{timeRange}</span>}
|
|
1247
|
-
{(totalIn > 0 || totalOut > 0) && (
|
|
1248
|
-
<span className="font-mono">
|
|
1249
|
-
{formatTokens(totalIn)} in / {formatTokens(totalOut)} out
|
|
1250
|
-
</span>
|
|
1251
|
-
)}
|
|
1252
|
-
</div>
|
|
1253
|
-
</div>
|
|
1254
|
-
{contextSummary.models.length > 0 && (
|
|
1255
|
-
<button
|
|
1256
|
-
type="button"
|
|
1257
|
-
onClick={() => setDetailsOpen((value) => !value)}
|
|
1258
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 font-mono text-[11px] text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
|
|
1259
|
-
aria-expanded={detailsOpen}
|
|
1260
|
-
title={detailsOpen ? "Hide context details" : "Show context details"}
|
|
1261
|
-
>
|
|
1262
|
-
<span>{contextOverview}</span>
|
|
1263
|
-
{detailsOpen ? (
|
|
1264
|
-
<ChevronUp className="size-3.5" />
|
|
1265
|
-
) : (
|
|
1266
|
-
<ChevronDown className="size-3.5" />
|
|
1267
|
-
)}
|
|
1268
|
-
</button>
|
|
1269
|
-
)}
|
|
1270
|
-
<button
|
|
1271
|
-
type="button"
|
|
1272
|
-
onClick={handleCopyLink}
|
|
1273
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
|
|
1274
|
-
aria-label={copied ? "Copied session link" : "Copy session link"}
|
|
1275
|
-
title={copied ? "Copied session link" : "Copy session link"}
|
|
1276
|
-
>
|
|
1277
|
-
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
1278
|
-
</button>
|
|
1279
|
-
</div>
|
|
1280
|
-
{detailsOpen && <SessionContextSummaryPanel summary={contextSummary} />}
|
|
1281
|
-
</div>
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
export type ProxyViewerProps = {
|
|
1286
|
-
logs: CapturedLog[];
|
|
1287
|
-
sessions: string[];
|
|
1288
|
-
models: string[];
|
|
1289
|
-
selectedSession: string;
|
|
1290
|
-
selectedModel: string;
|
|
1291
|
-
onSessionChange: (session: string) => void;
|
|
1292
|
-
onModelChange: (model: string) => void;
|
|
1293
|
-
onClearAll: () => void;
|
|
1294
|
-
/** Delete persisted storage for the selected session, or all storage from the all-sessions view. */
|
|
1295
|
-
onDeleteCurrentView?: () => Promise<void>;
|
|
1296
|
-
/** Reload the selected/pinned session from stored logs without requiring a page refresh. */
|
|
1297
|
-
onReloadSession?: () => void;
|
|
1298
|
-
/** Clear only the logs whose ids are passed. Called by the per-group
|
|
1299
|
-
* Clear button on each conversation header. */
|
|
1300
|
-
onClearGroup: (ids: number[]) => void;
|
|
1301
|
-
/** Surface short-lived action feedback without adding sticky page chrome. */
|
|
1302
|
-
onNotify?: (notice: TransientToastInput) => void;
|
|
1303
|
-
onImportLogs?: (file: File) => Promise<ImportLogsResponse>;
|
|
1304
|
-
viewMode: "simple" | "full";
|
|
1305
|
-
captureMode: CaptureMode;
|
|
1306
|
-
/** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
|
|
1307
|
-
strip: boolean;
|
|
1308
|
-
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
1309
|
-
slowResponseThresholdSeconds: number;
|
|
1310
|
-
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
1311
|
-
timeDisplayFormat: TimeDisplayFormat;
|
|
1312
|
-
/** True while a pinned session page is waiting for its initial log scan. */
|
|
1313
|
-
isLoading?: boolean;
|
|
1314
|
-
/** Current truth for the all-sessions live stream. Omitted on stored session pages. */
|
|
1315
|
-
liveConnection?: LiveConnectionState;
|
|
1316
|
-
/** Retry the live stream immediately without discarding displayed logs. */
|
|
1317
|
-
onRetryLiveConnection?: () => void;
|
|
1318
|
-
/** Stop automatic live-stream retries while keeping displayed logs available. */
|
|
1319
|
-
onStopLiveConnection?: () => void;
|
|
1320
|
-
/** Cursor controls for history pages. Undefined keeps the live stream UI compact. */
|
|
1321
|
-
pagination?: LogPaginationControls;
|
|
1322
|
-
/** Active session log loading progress, shown near load/reload controls. */
|
|
1323
|
-
sessionLoadProgress?: SessionLoadProgress;
|
|
1324
|
-
/** Recoverable session history load failure; existing logs remain visible. */
|
|
1325
|
-
sessionLoadError?: string;
|
|
1326
|
-
/** Group/run member metadata attached to the pinned session id. */
|
|
1327
|
-
sessionMemberships?: SessionMembershipEvidence[];
|
|
1328
|
-
/** Hide the session filter dropdown. Used on `/session/$id` routes where
|
|
1329
|
-
* the session is already pinned by the URL and the dropdown would just
|
|
1330
|
-
* fight the URL state. */
|
|
1331
|
-
hideSessionFilter?: boolean;
|
|
1332
|
-
/** Session id pinned by a `/session/$id` route. Enables session-page chrome. */
|
|
1333
|
-
pinnedSessionId?: string;
|
|
1334
|
-
};
|
|
1335
|
-
|
|
1336
|
-
export function ProxyViewer({
|
|
1337
|
-
logs,
|
|
1338
|
-
sessions,
|
|
1339
|
-
selectedSession,
|
|
1340
|
-
selectedModel,
|
|
1341
|
-
onSessionChange,
|
|
1342
|
-
onClearAll,
|
|
1343
|
-
onDeleteCurrentView,
|
|
1344
|
-
onReloadSession,
|
|
1345
|
-
onClearGroup,
|
|
1346
|
-
onNotify,
|
|
1347
|
-
onImportLogs,
|
|
1348
|
-
viewMode,
|
|
1349
|
-
captureMode,
|
|
1350
|
-
strip,
|
|
1351
|
-
slowResponseThresholdSeconds,
|
|
1352
|
-
timeDisplayFormat,
|
|
1353
|
-
isLoading = false,
|
|
1354
|
-
liveConnection,
|
|
1355
|
-
onRetryLiveConnection,
|
|
1356
|
-
onStopLiveConnection,
|
|
1357
|
-
pagination,
|
|
1358
|
-
sessionLoadProgress,
|
|
1359
|
-
sessionLoadError,
|
|
1360
|
-
sessionMemberships = [],
|
|
1361
|
-
hideSessionFilter = false,
|
|
1362
|
-
pinnedSessionId,
|
|
1363
|
-
}: ProxyViewerProps): JSX.Element {
|
|
1364
|
-
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
1365
|
-
const { providers } = useProviders();
|
|
1366
|
-
const { uiScalePreference, resolvedUiScale, setUiScalePreference } = useUiScale();
|
|
1367
|
-
const [exporting, setExporting] = useState<ExportMode | null>(null);
|
|
1368
|
-
const [exportError, setExportError] = useState<string | null>(null);
|
|
1369
|
-
const [rawExportDialogOpen, setRawExportDialogOpen] = useState(false);
|
|
1370
|
-
const [importing, setImporting] = useState(false);
|
|
1371
|
-
const [importStatus, setImportStatus] = useState<string | null>(null);
|
|
1372
|
-
const [importError, setImportError] = useState<string | null>(null);
|
|
1373
|
-
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
1374
|
-
const [deletingCurrentView, setDeletingCurrentView] = useState(false);
|
|
1375
|
-
const [detailsPanelCollapsed, setDetailsPanelCollapsed] = useState(true);
|
|
1376
|
-
const [detailsPanelWidth, setDetailsPanelWidth] = useState(DETAILS_PANEL_DEFAULT_WIDTH);
|
|
1377
|
-
const [detailsPanelResizing, setDetailsPanelResizing] = useState(false);
|
|
1378
|
-
const [detailsPanelStorageLoaded, setDetailsPanelStorageLoaded] = useState(false);
|
|
1379
|
-
const [petVisible, setPetVisible] = useState(true);
|
|
1380
|
-
const [petVisibilityStorageLoaded, setPetVisibilityStorageLoaded] = useState(false);
|
|
1381
|
-
const [assistantSearchResult, setAssistantSearchResult] = useState<PiAgentSearchResult | null>(
|
|
1382
|
-
null,
|
|
1383
|
-
);
|
|
1384
|
-
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
1385
|
-
const logListRef = useRef<HTMLDivElement>(null);
|
|
1386
|
-
const logListWrapperRef = useRef<HTMLDivElement>(null);
|
|
1387
|
-
const importInputRef = useRef<HTMLInputElement>(null);
|
|
1388
|
-
const exportAbortRef = useRef<AbortController | null>(null);
|
|
1389
|
-
const exportRequestIdRef = useRef(0);
|
|
1390
|
-
const detailsPanelResizeRef = useRef<DetailsPanelResizeState | null>(null);
|
|
1391
|
-
useKeyboardNavigation(logListRef, logListWrapperRef, { pageWide: true });
|
|
1392
|
-
const [connectionEndpoints, setConnectionEndpoints] = useState<CodingAgentEndpoints | null>(null);
|
|
1393
|
-
|
|
1394
|
-
useEffect(() => subscribeCodingAgentEndpoints(setConnectionEndpoints), []);
|
|
1395
|
-
|
|
1396
|
-
const cancelTransientTasks = useCallback(() => {
|
|
1397
|
-
exportRequestIdRef.current += 1;
|
|
1398
|
-
exportAbortRef.current?.abort();
|
|
1399
|
-
exportAbortRef.current = null;
|
|
1400
|
-
setExporting(null);
|
|
1401
|
-
setExportError(null);
|
|
1402
|
-
}, []);
|
|
1403
|
-
|
|
1404
|
-
useEffect(
|
|
1405
|
-
() => () => {
|
|
1406
|
-
exportRequestIdRef.current += 1;
|
|
1407
|
-
exportAbortRef.current?.abort();
|
|
1408
|
-
exportAbortRef.current = null;
|
|
1409
|
-
},
|
|
1410
|
-
[],
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
useEffect(() => {
|
|
1414
|
-
setDetailsPanelCollapsed(readStoredDetailsPanelCollapsed());
|
|
1415
|
-
setDetailsPanelWidth(readStoredDetailsPanelWidth());
|
|
1416
|
-
setDetailsPanelStorageLoaded(true);
|
|
1417
|
-
}, []);
|
|
1418
|
-
|
|
1419
|
-
useEffect(() => {
|
|
1420
|
-
if (!detailsPanelStorageLoaded) return;
|
|
1421
|
-
window.localStorage.setItem(
|
|
1422
|
-
DETAILS_PANEL_COLLAPSED_STORAGE_KEY,
|
|
1423
|
-
detailsPanelCollapsed ? "1" : "0",
|
|
1424
|
-
);
|
|
1425
|
-
}, [detailsPanelCollapsed, detailsPanelStorageLoaded]);
|
|
1426
|
-
|
|
1427
|
-
useEffect(() => {
|
|
1428
|
-
if (!detailsPanelStorageLoaded) return;
|
|
1429
|
-
try {
|
|
1430
|
-
window.localStorage.setItem(DETAILS_PANEL_WIDTH_STORAGE_KEY, String(detailsPanelWidth));
|
|
1431
|
-
} catch {
|
|
1432
|
-
// Local storage can be unavailable in hardened browser profiles.
|
|
1433
|
-
}
|
|
1434
|
-
}, [detailsPanelStorageLoaded, detailsPanelWidth]);
|
|
1435
|
-
|
|
1436
|
-
useEffect(() => {
|
|
1437
|
-
setPetVisible(readStoredInspectorPetVisible());
|
|
1438
|
-
setPetVisibilityStorageLoaded(true);
|
|
1439
|
-
}, []);
|
|
1440
|
-
|
|
1441
|
-
useEffect(() => {
|
|
1442
|
-
if (!petVisibilityStorageLoaded) return;
|
|
1443
|
-
try {
|
|
1444
|
-
window.localStorage.setItem(INSPECTOR_PET_VISIBLE_STORAGE_KEY, petVisible ? "1" : "0");
|
|
1445
|
-
} catch {
|
|
1446
|
-
// Local storage can be unavailable in hardened browser profiles.
|
|
1447
|
-
}
|
|
1448
|
-
}, [petVisibilityStorageLoaded, petVisible]);
|
|
1449
|
-
|
|
1450
|
-
useEffect(() => {
|
|
1451
|
-
if (pinnedSessionId === undefined) {
|
|
1452
|
-
document.title = "Agent Inspector";
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
const requestLabel = logs.length === 1 ? "1 req" : `${logs.length} req`;
|
|
1456
|
-
document.title = `${truncateSessionId(pinnedSessionId)} - ${requestLabel} - Agent Inspector`;
|
|
1457
|
-
}, [logs.length, pinnedSessionId]);
|
|
1458
|
-
|
|
1459
|
-
const handleExport = useCallback(
|
|
1460
|
-
async (mode: ExportMode) => {
|
|
1461
|
-
exportAbortRef.current?.abort();
|
|
1462
|
-
const controller = new AbortController();
|
|
1463
|
-
exportAbortRef.current = controller;
|
|
1464
|
-
const requestId = exportRequestIdRef.current + 1;
|
|
1465
|
-
exportRequestIdRef.current = requestId;
|
|
1466
|
-
setExporting(mode);
|
|
1467
|
-
setExportError(null);
|
|
1468
|
-
try {
|
|
1469
|
-
const result = await exportLogsAsZip(logs, mode, { signal: controller.signal });
|
|
1470
|
-
if (exportRequestIdRef.current !== requestId) return;
|
|
1471
|
-
if (!result.ok) {
|
|
1472
|
-
if (result.canceled === true) return;
|
|
1473
|
-
if (onNotify !== undefined) {
|
|
1474
|
-
onNotify({ message: result.message, tone: "error" });
|
|
1475
|
-
} else {
|
|
1476
|
-
setExportError(result.message);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
} finally {
|
|
1480
|
-
if (exportRequestIdRef.current === requestId) {
|
|
1481
|
-
exportAbortRef.current = null;
|
|
1482
|
-
setExporting(null);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
},
|
|
1486
|
-
[logs, onNotify],
|
|
1487
|
-
);
|
|
1488
|
-
const handleExportRequest = useCallback(
|
|
1489
|
-
(mode: ExportMode) => {
|
|
1490
|
-
if (exportRequiresConfirmation(mode)) {
|
|
1491
|
-
setRawExportDialogOpen(true);
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
void handleExport(mode);
|
|
1495
|
-
},
|
|
1496
|
-
[handleExport],
|
|
1497
|
-
);
|
|
1498
|
-
const handleExportRedactedInstead = useCallback(() => {
|
|
1499
|
-
setRawExportDialogOpen(false);
|
|
1500
|
-
void handleExport("redacted");
|
|
1501
|
-
}, [handleExport]);
|
|
1502
|
-
const handleConfirmRawExport = useCallback(() => {
|
|
1503
|
-
setRawExportDialogOpen(false);
|
|
1504
|
-
void handleExport("raw");
|
|
1505
|
-
}, [handleExport]);
|
|
1506
|
-
const handleImportClick = useCallback(() => {
|
|
1507
|
-
importInputRef.current?.click();
|
|
1508
|
-
}, []);
|
|
1509
|
-
|
|
1510
|
-
const handleImportFileChange = useCallback(
|
|
1511
|
-
(event: ChangeEvent<HTMLInputElement>) => {
|
|
1512
|
-
const file = event.currentTarget.files?.[0];
|
|
1513
|
-
event.currentTarget.value = "";
|
|
1514
|
-
if (file === undefined || onImportLogs === undefined) return;
|
|
1515
|
-
|
|
1516
|
-
setImporting(true);
|
|
1517
|
-
setImportError(null);
|
|
1518
|
-
setImportStatus(null);
|
|
1519
|
-
void onImportLogs(file)
|
|
1520
|
-
.then((result) => {
|
|
1521
|
-
const skipped = result.skipped > 0 ? `, skipped ${String(result.skipped)}` : "";
|
|
1522
|
-
const message = `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`;
|
|
1523
|
-
if (onNotify !== undefined) {
|
|
1524
|
-
onNotify({ message, tone: "success" });
|
|
1525
|
-
} else {
|
|
1526
|
-
setImportStatus(message);
|
|
1527
|
-
}
|
|
1528
|
-
})
|
|
1529
|
-
.catch((err: unknown) => {
|
|
1530
|
-
const message = err instanceof Error ? err.message : "Failed to import logs";
|
|
1531
|
-
if (onNotify !== undefined) {
|
|
1532
|
-
onNotify({ message, tone: "error" });
|
|
1533
|
-
} else {
|
|
1534
|
-
setImportError(message);
|
|
1535
|
-
}
|
|
1536
|
-
})
|
|
1537
|
-
.finally(() => {
|
|
1538
|
-
setImporting(false);
|
|
1539
|
-
});
|
|
1540
|
-
},
|
|
1541
|
-
[onImportLogs, onNotify],
|
|
1542
|
-
);
|
|
1543
|
-
// Close the compare drawer when the user changes the session or model
|
|
1544
|
-
// filter, since the predecessor relationship may no longer be meaningful.
|
|
1545
|
-
useEffect(() => {
|
|
1546
|
-
cancelTransientTasks();
|
|
1547
|
-
setComparePair(null);
|
|
1548
|
-
setAssistantSearchResult(null);
|
|
1549
|
-
}, [cancelTransientTasks, selectedSession, selectedModel]);
|
|
1550
|
-
|
|
1551
|
-
const handleClearCurrentView = useCallback(() => {
|
|
1552
|
-
cancelTransientTasks();
|
|
1553
|
-
setImportStatus(null);
|
|
1554
|
-
setImportError(null);
|
|
1555
|
-
setComparePair(null);
|
|
1556
|
-
onClearAll();
|
|
1557
|
-
}, [cancelTransientTasks, onClearAll]);
|
|
1558
|
-
|
|
1559
|
-
const handleRequestDeleteCurrentView = useCallback(() => {
|
|
1560
|
-
setDeleteDialogOpen(true);
|
|
1561
|
-
}, []);
|
|
1562
|
-
|
|
1563
|
-
const handleConfirmDeleteCurrentView = useCallback(() => {
|
|
1564
|
-
cancelTransientTasks();
|
|
1565
|
-
setImportStatus(null);
|
|
1566
|
-
setImportError(null);
|
|
1567
|
-
setComparePair(null);
|
|
1568
|
-
|
|
1569
|
-
if (onDeleteCurrentView === undefined) return;
|
|
1570
|
-
|
|
1571
|
-
setDeletingCurrentView(true);
|
|
1572
|
-
void onDeleteCurrentView()
|
|
1573
|
-
.then(() => {
|
|
1574
|
-
setDeleteDialogOpen(false);
|
|
1575
|
-
})
|
|
1576
|
-
.catch((err: unknown) => {
|
|
1577
|
-
const message = err instanceof Error ? err.message : "Failed to delete stored logs";
|
|
1578
|
-
if (onNotify !== undefined) {
|
|
1579
|
-
onNotify({ message, tone: "error" });
|
|
1580
|
-
} else {
|
|
1581
|
-
setImportError(message);
|
|
1582
|
-
}
|
|
1583
|
-
})
|
|
1584
|
-
.finally(() => {
|
|
1585
|
-
setDeletingCurrentView(false);
|
|
1586
|
-
});
|
|
1587
|
-
}, [cancelTransientTasks, onDeleteCurrentView, onNotify]);
|
|
1588
|
-
|
|
1589
|
-
const handleReloadCurrentSession = useCallback(() => {
|
|
1590
|
-
if (onReloadSession === undefined) return;
|
|
1591
|
-
cancelTransientTasks();
|
|
1592
|
-
setImportStatus(null);
|
|
1593
|
-
setImportError(null);
|
|
1594
|
-
setComparePair(null);
|
|
1595
|
-
onReloadSession();
|
|
1596
|
-
}, [cancelTransientTasks, onReloadSession]);
|
|
1597
|
-
|
|
1598
|
-
const closeCompare = useCallback(() => {
|
|
1599
|
-
setComparePair(null);
|
|
1600
|
-
}, []);
|
|
1601
|
-
|
|
1602
|
-
const toggleDetailsPanelCollapsed = useCallback(() => {
|
|
1603
|
-
setDetailsPanelCollapsed((value) => !value);
|
|
1604
|
-
}, []);
|
|
1605
|
-
const handleDetailsPanelResizeStart = useCallback(
|
|
1606
|
-
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1607
|
-
detailsPanelResizeRef.current = {
|
|
1608
|
-
startX: event.clientX,
|
|
1609
|
-
startWidth: detailsPanelWidth,
|
|
1610
|
-
pointerId: event.pointerId,
|
|
1611
|
-
};
|
|
1612
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1613
|
-
setDetailsPanelResizing(true);
|
|
1614
|
-
},
|
|
1615
|
-
[detailsPanelWidth],
|
|
1616
|
-
);
|
|
1617
|
-
const handleDetailsPanelResizeMove = useCallback(
|
|
1618
|
-
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1619
|
-
const resize = detailsPanelResizeRef.current;
|
|
1620
|
-
if (resize === null || resize.pointerId !== event.pointerId) return;
|
|
1621
|
-
setDetailsPanelWidth(
|
|
1622
|
-
clampDetailsPanelWidth(resize.startWidth + resize.startX - event.clientX),
|
|
1623
|
-
);
|
|
1624
|
-
},
|
|
1625
|
-
[],
|
|
1626
|
-
);
|
|
1627
|
-
const handleDetailsPanelResizeEnd = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1628
|
-
const resize = detailsPanelResizeRef.current;
|
|
1629
|
-
if (resize === null || resize.pointerId !== event.pointerId) return;
|
|
1630
|
-
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
1631
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1632
|
-
}
|
|
1633
|
-
detailsPanelResizeRef.current = null;
|
|
1634
|
-
setDetailsPanelResizing(false);
|
|
1635
|
-
}, []);
|
|
1636
|
-
const resetDetailsPanelWidth = useCallback(() => {
|
|
1637
|
-
setDetailsPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
|
|
1638
|
-
}, []);
|
|
1639
|
-
const togglePetVisibility = useCallback(() => {
|
|
1640
|
-
setPetVisible((value) => !value);
|
|
1641
|
-
}, []);
|
|
1642
|
-
|
|
1643
|
-
const assistantResultLogIds = useMemo(
|
|
1644
|
-
() => (assistantSearchResult === null ? null : new Set<number>(assistantSearchResult.logIds)),
|
|
1645
|
-
[assistantSearchResult],
|
|
1646
|
-
);
|
|
1647
|
-
const displayedLogs = useMemo(
|
|
1648
|
-
() =>
|
|
1649
|
-
assistantResultLogIds === null
|
|
1650
|
-
? logs
|
|
1651
|
-
: logs.filter((log) => assistantResultLogIds.has(log.id)),
|
|
1652
|
-
[assistantResultLogIds, logs],
|
|
1653
|
-
);
|
|
1654
|
-
const groups = useMemo(() => groupLogsByConversation(displayedLogs), [displayedLogs]);
|
|
1655
|
-
const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
|
|
1656
|
-
const sessionContextScope = useMemo(
|
|
1657
|
-
() => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
|
|
1658
|
-
[logs, pinnedSessionId, selectedSession, sessions],
|
|
1659
|
-
);
|
|
1660
|
-
const sessionContextLogs = sessionContextScope?.logs ?? [];
|
|
1661
|
-
const hasSessionContext = sessionContextScope !== null && sessionContextLogs.length > 0;
|
|
1662
|
-
const { totalIn: sessionContextTotalIn, totalOut: sessionContextTotalOut } = useMemo(
|
|
1663
|
-
() => computeTokenSummary(sessionContextLogs),
|
|
1664
|
-
[sessionContextLogs],
|
|
1665
|
-
);
|
|
1666
|
-
const sessionContextSummary = useMemo(
|
|
1667
|
-
() => buildSessionContextSummary(sessionContextLogs, providers),
|
|
1668
|
-
[sessionContextLogs, providers],
|
|
1669
|
-
);
|
|
1670
|
-
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
1671
|
-
const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
|
|
1672
|
-
const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
|
|
1673
|
-
const isPinnedSessionPage = pinnedSessionId !== undefined;
|
|
1674
|
-
const canClearCurrentView = logs.length > 0;
|
|
1675
|
-
const canDeleteCurrentView =
|
|
1676
|
-
onDeleteCurrentView !== undefined && (logs.length > 0 || selectedSession !== "__all__");
|
|
1677
|
-
const deleteTargetLabel = selectedSession === "__all__" ? "all sessions" : "this session";
|
|
1678
|
-
const deleteTargetDescription =
|
|
1679
|
-
selectedSession === "__all__"
|
|
1680
|
-
? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
|
|
1681
|
-
: "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
|
|
1682
|
-
const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
|
|
1683
|
-
const activeScopeLabel =
|
|
1684
|
-
hasSessionContext && sessionContextScope !== null
|
|
1685
|
-
? truncateSessionId(sessionContextScope.label)
|
|
1686
|
-
: selectedSession === "__all__"
|
|
1687
|
-
? "All sessions"
|
|
1688
|
-
: truncateSessionId(selectedSession);
|
|
1689
|
-
const liveEmptyState = resolveLiveEmptyState(liveConnection);
|
|
1690
|
-
const liveEmptyStateCopy = getLiveEmptyStateCopy(liveEmptyState);
|
|
1691
|
-
const slateStats = useMemo(
|
|
1692
|
-
() =>
|
|
1693
|
-
buildSessionSlateStats({
|
|
1694
|
-
logs: displayedLogs,
|
|
1695
|
-
conversationGroups: groups,
|
|
1696
|
-
isLoading,
|
|
1697
|
-
timeDisplayFormat,
|
|
1698
|
-
slowResponseThresholdSeconds,
|
|
1699
|
-
}),
|
|
1700
|
-
[displayedLogs, groups, isLoading, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1701
|
-
);
|
|
1702
|
-
const handleCompareWithPrevious = useCallback(
|
|
1703
|
-
(log: CapturedLog) => {
|
|
1704
|
-
const predecessor = comparisonPredecessors.get(log.id);
|
|
1705
|
-
if (predecessor !== undefined) setComparePair([predecessor, log]);
|
|
1706
|
-
},
|
|
1707
|
-
[comparisonPredecessors],
|
|
1708
|
-
);
|
|
1709
|
-
const labSessionId =
|
|
1710
|
-
pinnedSessionId !== undefined
|
|
1711
|
-
? pinnedSessionId
|
|
1712
|
-
: selectedSession === "__all__"
|
|
1713
|
-
? null
|
|
1714
|
-
: selectedSession;
|
|
1715
|
-
|
|
1716
|
-
return (
|
|
1717
|
-
<div className="bg-background min-h-screen w-full overflow-x-clip text-foreground lg:h-screen lg:flex lg:flex-col lg:overflow-hidden">
|
|
1718
|
-
<ApplicationBar
|
|
1719
|
-
liveConnection={liveConnection}
|
|
1720
|
-
alertsAction={<AlertsDialog />}
|
|
1721
|
-
settingsAction={<SettingsDialog />}
|
|
1722
|
-
/>
|
|
1723
|
-
<button
|
|
1724
|
-
type="button"
|
|
1725
|
-
onClick={toggleDetailsPanelCollapsed}
|
|
1726
|
-
className={cn(
|
|
1727
|
-
"group fixed top-1/2 right-3 z-50 inline-flex h-14 w-8 -translate-y-1/2 items-center justify-center rounded-l-md border border-border/70 bg-background/90 text-muted-foreground shadow-lg backdrop-blur transition-colors motion-reduce:transition-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
|
1728
|
-
)}
|
|
1729
|
-
aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
|
|
1730
|
-
title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
|
|
1731
|
-
>
|
|
1732
|
-
<span className="absolute bottom-2 left-1 top-2 w-px rounded-full bg-border transition-colors group-hover:bg-foreground/35" />
|
|
1733
|
-
{detailsPanelCollapsed ? (
|
|
1734
|
-
<PanelRightOpen className="size-3.5" />
|
|
1735
|
-
) : (
|
|
1736
|
-
<PanelRightClose className="size-3.5" />
|
|
1737
|
-
)}
|
|
1738
|
-
</button>
|
|
1739
|
-
{petVisibilityStorageLoaded && petVisible && (
|
|
1740
|
-
<LazyFeatureBoundary feature="Inspector Pet">
|
|
1741
|
-
<Suspense fallback={null}>
|
|
1742
|
-
<LazyInspectorPet logs={logs} />
|
|
1743
|
-
</Suspense>
|
|
1744
|
-
</LazyFeatureBoundary>
|
|
1745
|
-
)}
|
|
1746
|
-
<div
|
|
1747
|
-
className={cn(
|
|
1748
|
-
"flex w-full min-w-0 flex-col lg:min-h-0 lg:flex-1 lg:flex-row lg:overflow-hidden",
|
|
1749
|
-
detailsPanelResizing && "cursor-col-resize select-none",
|
|
1750
|
-
)}
|
|
1751
|
-
>
|
|
1752
|
-
{!detailsPanelCollapsed && (
|
|
1753
|
-
<>
|
|
1754
|
-
<button
|
|
1755
|
-
type="button"
|
|
1756
|
-
className="fixed inset-0 z-30 bg-background/70 backdrop-blur-sm lg:hidden"
|
|
1757
|
-
onClick={toggleDetailsPanelCollapsed}
|
|
1758
|
-
aria-label="Close details panel"
|
|
1759
|
-
/>
|
|
1760
|
-
<aside
|
|
1761
|
-
className="inspector-scrollbar fixed inset-x-3 bottom-3 top-24 z-40 order-1 overflow-y-auto rounded-lg border border-border bg-background/98 shadow-2xl max-lg:!w-auto lg:relative lg:inset-auto lg:order-2 lg:h-full lg:shrink-0 lg:rounded-none lg:border-y-0 lg:border-r-0 lg:shadow-none"
|
|
1762
|
-
style={{ width: detailsPanelWidth }}
|
|
1763
|
-
>
|
|
1764
|
-
<button
|
|
1765
|
-
type="button"
|
|
1766
|
-
className="absolute bottom-0 left-0 top-0 z-30 hidden w-2 -translate-x-1/2 cursor-col-resize touch-none items-center justify-center lg:flex"
|
|
1767
|
-
onPointerDown={handleDetailsPanelResizeStart}
|
|
1768
|
-
onPointerMove={handleDetailsPanelResizeMove}
|
|
1769
|
-
onPointerUp={handleDetailsPanelResizeEnd}
|
|
1770
|
-
onPointerCancel={handleDetailsPanelResizeEnd}
|
|
1771
|
-
onDoubleClick={resetDetailsPanelWidth}
|
|
1772
|
-
aria-label="Resize details panel"
|
|
1773
|
-
title="Drag to resize; double-click to reset"
|
|
1774
|
-
>
|
|
1775
|
-
<span className="h-12 w-1 rounded-full bg-border transition-colors hover:bg-primary" />
|
|
1776
|
-
</button>
|
|
1777
|
-
<div className="flex min-h-full flex-col gap-4 p-4">
|
|
1778
|
-
<div className="sticky top-0 z-20 -mx-4 -mt-4 grid gap-2 border-b border-border/60 bg-background/95 px-4 pb-3 pt-4 backdrop-blur">
|
|
1779
|
-
<SidebarPanel label="View">
|
|
1780
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
1781
|
-
<ThemeModeToggle />
|
|
1782
|
-
<UiScaleControl
|
|
1783
|
-
preference={uiScalePreference}
|
|
1784
|
-
resolvedScale={resolvedUiScale}
|
|
1785
|
-
onChange={setUiScalePreference}
|
|
1786
|
-
/>
|
|
1787
|
-
</div>
|
|
1788
|
-
</SidebarPanel>
|
|
1789
|
-
<SidebarPanel label="Workspace">
|
|
1790
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1791
|
-
<GroupsDialog labelledTrigger />
|
|
1792
|
-
<AgentLabDialog
|
|
1793
|
-
currentSessionId={labSessionId}
|
|
1794
|
-
logCount={logs.length}
|
|
1795
|
-
labelledTrigger
|
|
1796
|
-
/>
|
|
1797
|
-
</div>
|
|
1798
|
-
</SidebarPanel>
|
|
1799
|
-
</div>
|
|
1800
|
-
|
|
1801
|
-
<SidebarPanel label="Current scope">
|
|
1802
|
-
<div className="mb-2 rounded-md border border-border/50 bg-muted/25 px-3 py-2">
|
|
1803
|
-
<div
|
|
1804
|
-
className="truncate font-mono text-xs font-semibold text-text-link"
|
|
1805
|
-
title={activeScopeLabel}
|
|
1806
|
-
>
|
|
1807
|
-
{activeScopeLabel}
|
|
1808
|
-
</div>
|
|
1809
|
-
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
1810
|
-
{isPinnedSessionPage ? "Pinned session" : "Live Inspector data"}
|
|
1811
|
-
</div>
|
|
1812
|
-
</div>
|
|
1813
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1814
|
-
<SidebarMetric
|
|
1815
|
-
label={assistantSearchResult === null ? "Requests" : "Results"}
|
|
1816
|
-
value={displayedLogs.length}
|
|
1817
|
-
/>
|
|
1818
|
-
<SidebarMetric label="Sessions" value={sessions.length} />
|
|
1819
|
-
<SidebarMetric
|
|
1820
|
-
label="Input"
|
|
1821
|
-
value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
|
|
1822
|
-
/>
|
|
1823
|
-
<SidebarMetric
|
|
1824
|
-
label="Output"
|
|
1825
|
-
value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
|
|
1826
|
-
/>
|
|
1827
|
-
</div>
|
|
1828
|
-
{sessionLoadProgress !== undefined && (
|
|
1829
|
-
<div className="mt-2 rounded-md border border-border/50 bg-muted/25 px-2 py-2">
|
|
1830
|
-
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
1831
|
-
</div>
|
|
1832
|
-
)}
|
|
1833
|
-
</SidebarPanel>
|
|
1834
|
-
|
|
1835
|
-
{hasSessionContext && sessionContextScope !== null && (
|
|
1836
|
-
<SessionContextBar
|
|
1837
|
-
label={sessionContextScope.label}
|
|
1838
|
-
logs={sessionContextLogs}
|
|
1839
|
-
totalIn={sessionContextTotalIn}
|
|
1840
|
-
totalOut={sessionContextTotalOut}
|
|
1841
|
-
timeDisplayFormat={timeDisplayFormat}
|
|
1842
|
-
contextSummary={sessionContextSummary}
|
|
1843
|
-
showBackLink={sessionContextScope.showBackLink}
|
|
1844
|
-
/>
|
|
1845
|
-
)}
|
|
1846
|
-
|
|
1847
|
-
{hasSessionContext &&
|
|
1848
|
-
pinnedSessionId !== undefined &&
|
|
1849
|
-
sessionMemberships.length > 0 && (
|
|
1850
|
-
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
1851
|
-
)}
|
|
1852
|
-
|
|
1853
|
-
<div className="flex-1" />
|
|
1854
|
-
|
|
1855
|
-
<PiAgentPanel
|
|
1856
|
-
currentSessionId={labSessionId}
|
|
1857
|
-
logCount={logs.length}
|
|
1858
|
-
onResultChange={setAssistantSearchResult}
|
|
1859
|
-
/>
|
|
1860
|
-
</div>
|
|
1861
|
-
</aside>
|
|
1862
|
-
</>
|
|
1863
|
-
)}
|
|
1864
|
-
|
|
1865
|
-
<main className="inspector-scrollbar order-2 min-w-0 lg:order-1 lg:h-full lg:flex-1 lg:overflow-y-auto">
|
|
1866
|
-
<div className="bg-background/95 shadow-[0_12px_36px_rgba(0,0,0,0.18),inset_0_-1px_0_rgba(255,255,255,0.045)] sticky top-0 z-30 px-4 py-3 lg:px-5">
|
|
1867
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1868
|
-
<div className="min-w-[180px] flex-1">
|
|
1869
|
-
<div className="text-sm font-semibold text-foreground">Logs</div>
|
|
1870
|
-
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground">
|
|
1871
|
-
{activeScopeLabel}
|
|
1872
|
-
</div>
|
|
1873
|
-
</div>
|
|
1874
|
-
<PetVisibilityToggle visible={petVisible} onToggle={togglePetVisibility} />
|
|
1875
|
-
{onImportLogs !== undefined && (
|
|
1876
|
-
<>
|
|
1877
|
-
<input
|
|
1878
|
-
ref={importInputRef}
|
|
1879
|
-
type="file"
|
|
1880
|
-
accept=".zip,.json,.jsonl,application/zip,application/json"
|
|
1881
|
-
className="hidden"
|
|
1882
|
-
onChange={handleImportFileChange}
|
|
1883
|
-
/>
|
|
1884
|
-
<button
|
|
1885
|
-
type="button"
|
|
1886
|
-
onClick={handleImportClick}
|
|
1887
|
-
disabled={importing}
|
|
1888
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1889
|
-
title="Import Agent Inspector export"
|
|
1890
|
-
>
|
|
1891
|
-
{importing ? (
|
|
1892
|
-
<span>Importing...</span>
|
|
1893
|
-
) : (
|
|
1894
|
-
<>
|
|
1895
|
-
<Upload className="size-3.5" />
|
|
1896
|
-
<span>Import</span>
|
|
1897
|
-
</>
|
|
1898
|
-
)}
|
|
1899
|
-
</button>
|
|
1900
|
-
</>
|
|
1901
|
-
)}
|
|
1902
|
-
{exportActionVisibility.redacted && (
|
|
1903
|
-
<button
|
|
1904
|
-
type="button"
|
|
1905
|
-
onClick={() => {
|
|
1906
|
-
handleExportRequest("redacted");
|
|
1907
|
-
}}
|
|
1908
|
-
disabled={exporting !== null}
|
|
1909
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1910
|
-
title="Export redacted logs as JSON ZIP"
|
|
1911
|
-
>
|
|
1912
|
-
{exporting === "redacted" ? (
|
|
1913
|
-
<span>Exporting...</span>
|
|
1914
|
-
) : (
|
|
1915
|
-
<>
|
|
1916
|
-
<Download className="size-3.5" />
|
|
1917
|
-
<span>Export</span>
|
|
1918
|
-
</>
|
|
1919
|
-
)}
|
|
1920
|
-
</button>
|
|
1921
|
-
)}
|
|
1922
|
-
{exportActionVisibility.raw && (
|
|
1923
|
-
<button
|
|
1924
|
-
type="button"
|
|
1925
|
-
onClick={() => {
|
|
1926
|
-
handleExportRequest("raw");
|
|
1927
|
-
}}
|
|
1928
|
-
disabled={exporting !== null}
|
|
1929
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1930
|
-
title="Export raw logs without redaction"
|
|
1931
|
-
>
|
|
1932
|
-
{exporting === "raw" ? (
|
|
1933
|
-
<span>Exporting...</span>
|
|
1934
|
-
) : (
|
|
1935
|
-
<>
|
|
1936
|
-
<FileJson className="size-3.5" />
|
|
1937
|
-
<span>Raw</span>
|
|
1938
|
-
</>
|
|
1939
|
-
)}
|
|
1940
|
-
</button>
|
|
1941
|
-
)}
|
|
1942
|
-
{onReloadSession !== undefined && (
|
|
1943
|
-
<button
|
|
1944
|
-
type="button"
|
|
1945
|
-
onClick={handleReloadCurrentSession}
|
|
1946
|
-
disabled={isLoading}
|
|
1947
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1948
|
-
title="Reload this session from stored logs"
|
|
1949
|
-
>
|
|
1950
|
-
<RefreshCw
|
|
1951
|
-
className={cn(
|
|
1952
|
-
"size-3.5",
|
|
1953
|
-
isLoading && "animate-spin motion-reduce:animate-none",
|
|
1954
|
-
)}
|
|
1955
|
-
/>
|
|
1956
|
-
<span>{reloadSessionLabel}</span>
|
|
1957
|
-
</button>
|
|
1958
|
-
)}
|
|
1959
|
-
<button
|
|
1960
|
-
type="button"
|
|
1961
|
-
onClick={handleClearCurrentView}
|
|
1962
|
-
disabled={!canClearCurrentView}
|
|
1963
|
-
className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1964
|
-
title="Clear the current view. Stored logs remain reloadable."
|
|
1965
|
-
>
|
|
1966
|
-
<Trash2 className="size-3.5" />
|
|
1967
|
-
<span>Clear</span>
|
|
1968
|
-
</button>
|
|
1969
|
-
{onDeleteCurrentView !== undefined && (
|
|
1970
|
-
<button
|
|
1971
|
-
type="button"
|
|
1972
|
-
onClick={handleRequestDeleteCurrentView}
|
|
1973
|
-
disabled={!canDeleteCurrentView || deletingCurrentView}
|
|
1974
|
-
className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 focus-visible:ring-1 focus-visible:ring-red-300 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1975
|
-
title="Delete persisted history for the selected session."
|
|
1976
|
-
>
|
|
1977
|
-
<Trash2 className="size-3.5" />
|
|
1978
|
-
<span>Delete</span>
|
|
1979
|
-
</button>
|
|
1980
|
-
)}
|
|
1981
|
-
</div>
|
|
1982
|
-
{pagination === undefined && sessionLoadProgress !== undefined && (
|
|
1983
|
-
<div className="mt-2 rounded-md bg-black/20 px-2 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
|
|
1984
|
-
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
1985
|
-
</div>
|
|
1986
|
-
)}
|
|
1987
|
-
{sessionLoadError !== undefined && (
|
|
1988
|
-
<div
|
|
1989
|
-
role="alert"
|
|
1990
|
-
className="mt-2 flex flex-wrap items-center gap-2 rounded-md border border-red-400/25 bg-red-500/10 px-3 py-2 text-xs text-red-100"
|
|
1991
|
-
>
|
|
1992
|
-
<span className="min-w-0 flex-1">
|
|
1993
|
-
Session logs could not be loaded. {sessionLoadError} Existing logs remain visible.
|
|
1994
|
-
</span>
|
|
1995
|
-
{onReloadSession !== undefined && (
|
|
1996
|
-
<button
|
|
1997
|
-
type="button"
|
|
1998
|
-
onClick={handleReloadCurrentSession}
|
|
1999
|
-
className="inline-flex h-7 items-center gap-1 rounded-md bg-black/15 px-2 font-medium transition-colors hover:bg-black/25 focus-visible:ring-1 focus-visible:ring-current focus-visible:outline-none"
|
|
2000
|
-
>
|
|
2001
|
-
<RefreshCw className="size-3" />
|
|
2002
|
-
Retry
|
|
2003
|
-
</button>
|
|
2004
|
-
)}
|
|
2005
|
-
</div>
|
|
2006
|
-
)}
|
|
2007
|
-
{assistantSearchResult !== null && (
|
|
2008
|
-
<div className="mt-2 flex items-center gap-3 rounded-md bg-cyan-400/[0.07] px-3 py-2 shadow-[inset_2px_0_0_rgba(103,232,249,0.35),inset_0_0_0_1px_rgba(103,232,249,0.08)]">
|
|
2009
|
-
<div className="min-w-0 flex-1">
|
|
2010
|
-
<div className="truncate text-xs font-medium text-cyan-50">
|
|
2011
|
-
{assistantSearchResult.question}
|
|
2012
|
-
</div>
|
|
2013
|
-
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
|
2014
|
-
Showing {String(displayedLogs.length)} loaded logs from{" "}
|
|
2015
|
-
{String(assistantSearchResult.logIds.length)} linked results
|
|
2016
|
-
</div>
|
|
2017
|
-
</div>
|
|
2018
|
-
<button
|
|
2019
|
-
type="button"
|
|
2020
|
-
onClick={() => setAssistantSearchResult(null)}
|
|
2021
|
-
className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md bg-black/20 px-2 text-[11px] text-muted-foreground transition-colors hover:bg-black/30 hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
2022
|
-
>
|
|
2023
|
-
<X className="size-3" />
|
|
2024
|
-
All logs
|
|
2025
|
-
</button>
|
|
2026
|
-
</div>
|
|
2027
|
-
)}
|
|
2028
|
-
{assistantSearchResult === null && (
|
|
2029
|
-
<LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
|
|
2030
|
-
)}
|
|
2031
|
-
{onNotify === undefined && exportError !== null && (
|
|
2032
|
-
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
2033
|
-
{exportError}
|
|
2034
|
-
</div>
|
|
2035
|
-
)}
|
|
2036
|
-
{onNotify === undefined && importError !== null && (
|
|
2037
|
-
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
2038
|
-
{importError}
|
|
2039
|
-
</div>
|
|
2040
|
-
)}
|
|
2041
|
-
{onNotify === undefined && importStatus !== null && importError === null && (
|
|
2042
|
-
<div className="mt-2 rounded-md border border-cyan-400/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100">
|
|
2043
|
-
{importStatus}
|
|
2044
|
-
</div>
|
|
2045
|
-
)}
|
|
2046
|
-
{liveConnection !== undefined &&
|
|
2047
|
-
liveConnection.status !== "live" &&
|
|
2048
|
-
onRetryLiveConnection !== undefined &&
|
|
2049
|
-
onStopLiveConnection !== undefined && (
|
|
2050
|
-
<LiveConnectionStatus
|
|
2051
|
-
state={liveConnection}
|
|
2052
|
-
hasLogs={logs.length > 0}
|
|
2053
|
-
onRetry={onRetryLiveConnection}
|
|
2054
|
-
onStop={onStopLiveConnection}
|
|
2055
|
-
/>
|
|
2056
|
-
)}
|
|
2057
|
-
</div>
|
|
2058
|
-
|
|
2059
|
-
<div className="px-4 py-4 lg:px-5">
|
|
2060
|
-
<SessionOpeningSlate stats={slateStats} />
|
|
2061
|
-
{assistantSearchResult !== null && displayedLogs.length === 0 ? (
|
|
2062
|
-
<div className="py-16 text-center text-muted-foreground">
|
|
2063
|
-
<div className="mx-auto max-w-xl rounded-[8px] bg-white/[0.035] px-5 py-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)]">
|
|
2064
|
-
<p className="text-sm font-medium text-foreground">No linked logs are loaded</p>
|
|
2065
|
-
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
|
2066
|
-
Assistant found evidence, but none of its linked log IDs are in the current
|
|
2067
|
-
loaded window. Open an evidence card in Assistant or restore the full log view.
|
|
2068
|
-
</p>
|
|
2069
|
-
<button
|
|
2070
|
-
type="button"
|
|
2071
|
-
onClick={() => setAssistantSearchResult(null)}
|
|
2072
|
-
className="mt-4 inline-flex h-8 items-center rounded-md bg-cyan-400/10 px-3 text-xs text-cyan-100 transition-colors hover:bg-cyan-400/16"
|
|
2073
|
-
>
|
|
2074
|
-
Show all logs
|
|
2075
|
-
</button>
|
|
2076
|
-
</div>
|
|
2077
|
-
</div>
|
|
2078
|
-
) : logs.length === 0 ? (
|
|
2079
|
-
selectedSession !== "__all__" ? (
|
|
2080
|
-
<div className="py-16 text-center text-muted-foreground">
|
|
2081
|
-
<div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
|
|
2082
|
-
<p className="text-sm font-medium">
|
|
2083
|
-
{isLoading
|
|
2084
|
-
? "Loading session data..."
|
|
2085
|
-
: sessionLoadError === undefined
|
|
2086
|
-
? "No logs loaded for this session"
|
|
2087
|
-
: "Session logs could not be loaded"}
|
|
2088
|
-
</p>
|
|
2089
|
-
<p className="inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all">
|
|
2090
|
-
{truncateSessionId(selectedSession)}
|
|
2091
|
-
</p>
|
|
2092
|
-
{sessionLoadProgress !== undefined && (
|
|
2093
|
-
<div className="mx-auto max-w-md rounded-md bg-black/20 px-2 py-2 text-left shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
|
|
2094
|
-
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
2095
|
-
</div>
|
|
2096
|
-
)}
|
|
2097
|
-
{sessionMemberships.length > 0 && (
|
|
2098
|
-
<div className="text-left">
|
|
2099
|
-
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
2100
|
-
</div>
|
|
2101
|
-
)}
|
|
2102
|
-
{!isLoading && (
|
|
2103
|
-
<div className="flex flex-col items-center gap-3 text-xs">
|
|
2104
|
-
<p>Stored logs may still be available.</p>
|
|
2105
|
-
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
2106
|
-
{onReloadSession !== undefined && (
|
|
2107
|
-
<button
|
|
2108
|
-
type="button"
|
|
2109
|
-
onClick={handleReloadCurrentSession}
|
|
2110
|
-
className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
2111
|
-
>
|
|
2112
|
-
<RefreshCw className="size-3.5" />
|
|
2113
|
-
<span>Load session</span>
|
|
2114
|
-
</button>
|
|
2115
|
-
)}
|
|
2116
|
-
{hideSessionFilter ? (
|
|
2117
|
-
<a
|
|
2118
|
-
href={withBasePath("/")}
|
|
2119
|
-
className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
2120
|
-
>
|
|
2121
|
-
Back to all sessions
|
|
2122
|
-
</a>
|
|
2123
|
-
) : (
|
|
2124
|
-
<button
|
|
2125
|
-
type="button"
|
|
2126
|
-
onClick={() => onSessionChange("__all__")}
|
|
2127
|
-
className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
2128
|
-
>
|
|
2129
|
-
Show all sessions
|
|
2130
|
-
</button>
|
|
2131
|
-
)}
|
|
2132
|
-
</div>
|
|
2133
|
-
</div>
|
|
2134
|
-
)}
|
|
2135
|
-
</div>
|
|
2136
|
-
</div>
|
|
2137
|
-
) : (
|
|
2138
|
-
<div className="py-16 text-center text-muted-foreground">
|
|
2139
|
-
<div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
|
|
2140
|
-
<p className="text-sm font-medium">{liveEmptyStateCopy.title}</p>
|
|
2141
|
-
<p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
|
|
2142
|
-
{liveEmptyStateCopy.description}
|
|
2143
|
-
</p>
|
|
2144
|
-
{liveEmptyState === "empty" && sessions.length > 0 && (
|
|
2145
|
-
<p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
|
|
2146
|
-
Pick a session from the filter panel to load stored logs on demand.
|
|
2147
|
-
</p>
|
|
2148
|
-
)}
|
|
2149
|
-
{liveEmptyStateCopy.showConnectionCommands && connectionEndpoints !== null && (
|
|
2150
|
-
<>
|
|
2151
|
-
<div className="flex flex-col items-center gap-2">
|
|
2152
|
-
{buildHomeConnectionCommands(connectionEndpoints).map((command) => (
|
|
2153
|
-
<CopyableCommand key={command} command={command} />
|
|
2154
|
-
))}
|
|
2155
|
-
</div>
|
|
2156
|
-
<p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
|
|
2157
|
-
Container note: if your AI tool runs in a different container or host than
|
|
2158
|
-
Agent Inspector, replace localhost with an address reachable from that
|
|
2159
|
-
tool and verify the network/firewall path.
|
|
2160
|
-
</p>
|
|
2161
|
-
</>
|
|
2162
|
-
)}
|
|
2163
|
-
</div>
|
|
2164
|
-
</div>
|
|
2165
|
-
)
|
|
2166
|
-
) : (
|
|
2167
|
-
<div
|
|
2168
|
-
ref={logListWrapperRef}
|
|
2169
|
-
tabIndex={0}
|
|
2170
|
-
aria-label="Captured logs"
|
|
2171
|
-
className="flex flex-col gap-2 rounded-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
2172
|
-
>
|
|
2173
|
-
<div ref={logListRef}>
|
|
2174
|
-
<ConversationGroupList
|
|
2175
|
-
groups={groups}
|
|
2176
|
-
viewMode={viewMode}
|
|
2177
|
-
strip={strip}
|
|
2178
|
-
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
2179
|
-
cacheTrends={cacheTrends}
|
|
2180
|
-
onCompareWithPrevious={handleCompareWithPrevious}
|
|
2181
|
-
comparisonPredecessors={comparisonPredecessors}
|
|
2182
|
-
onClearGroup={onClearGroup}
|
|
2183
|
-
defaultExpanded={groups.length === 1}
|
|
2184
|
-
timeDisplayFormat={timeDisplayFormat}
|
|
2185
|
-
/>
|
|
2186
|
-
</div>
|
|
2187
|
-
{assistantSearchResult === null && (
|
|
2188
|
-
<div className="mt-3">
|
|
2189
|
-
<LogPaginationBar logs={logs} pagination={pagination} />
|
|
2190
|
-
</div>
|
|
2191
|
-
)}
|
|
2192
|
-
</div>
|
|
2193
|
-
)}
|
|
2194
|
-
</div>
|
|
2195
|
-
</main>
|
|
2196
|
-
</div>
|
|
2197
|
-
|
|
2198
|
-
<Dialog open={rawExportDialogOpen} onOpenChange={setRawExportDialogOpen}>
|
|
2199
|
-
<DialogContent className={RAW_EXPORT_DIALOG_CLASS_NAME}>
|
|
2200
|
-
<RawExportWarning
|
|
2201
|
-
onCancel={() => setRawExportDialogOpen(false)}
|
|
2202
|
-
onExportRedacted={handleExportRedactedInstead}
|
|
2203
|
-
onConfirmRaw={handleConfirmRawExport}
|
|
2204
|
-
/>
|
|
2205
|
-
</DialogContent>
|
|
2206
|
-
</Dialog>
|
|
2207
|
-
|
|
2208
|
-
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
2209
|
-
<DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
|
|
2210
|
-
<DialogHeader>
|
|
2211
|
-
<DialogTitle>Delete {deleteTargetLabel} history?</DialogTitle>
|
|
2212
|
-
<DialogDescription>
|
|
2213
|
-
Clear only hides logs from the UI. Delete removes stored history from disk.
|
|
2214
|
-
</DialogDescription>
|
|
2215
|
-
</DialogHeader>
|
|
2216
|
-
<div className="rounded-md bg-red-500/[0.08] px-3 py-3 text-sm text-red-100 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.16)]">
|
|
2217
|
-
<span className="block font-medium">This is a full history delete.</span>
|
|
2218
|
-
<span className="mt-1 block text-xs text-red-200/80">{deleteTargetDescription}</span>
|
|
2219
|
-
</div>
|
|
2220
|
-
<div className="flex justify-end gap-2">
|
|
2221
|
-
<Button
|
|
2222
|
-
type="button"
|
|
2223
|
-
variant="outline"
|
|
2224
|
-
size="sm"
|
|
2225
|
-
disabled={deletingCurrentView}
|
|
2226
|
-
onClick={() => setDeleteDialogOpen(false)}
|
|
2227
|
-
>
|
|
2228
|
-
Cancel
|
|
2229
|
-
</Button>
|
|
2230
|
-
<Button
|
|
2231
|
-
type="button"
|
|
2232
|
-
variant="destructive"
|
|
2233
|
-
size="sm"
|
|
2234
|
-
disabled={deletingCurrentView}
|
|
2235
|
-
onClick={handleConfirmDeleteCurrentView}
|
|
2236
|
-
>
|
|
2237
|
-
{deletingCurrentView ? "Deleting..." : "Delete history"}
|
|
2238
|
-
</Button>
|
|
2239
|
-
</div>
|
|
2240
|
-
</DialogContent>
|
|
2241
|
-
</Dialog>
|
|
2242
|
-
|
|
2243
|
-
{/* Compare drawer: sibling of the log list, not a route change. */}
|
|
2244
|
-
{comparePair !== null && (
|
|
2245
|
-
<LazyFeatureBoundary feature="Compare">
|
|
2246
|
-
<Suspense fallback={null}>
|
|
2247
|
-
<LazyCompareDrawer
|
|
2248
|
-
left={comparePair[0]}
|
|
2249
|
-
right={comparePair[1]}
|
|
2250
|
-
leftDisplayNumber={displayNumberForLog(comparePair[0], displayNumberByLogId)}
|
|
2251
|
-
rightDisplayNumber={displayNumberForLog(comparePair[1], displayNumberByLogId)}
|
|
2252
|
-
onClose={closeCompare}
|
|
2253
|
-
/>
|
|
2254
|
-
</Suspense>
|
|
2255
|
-
</LazyFeatureBoundary>
|
|
2256
|
-
)}
|
|
2257
|
-
</div>
|
|
2258
|
-
);
|
|
2259
|
-
}
|