@tonyclaw/agent-inspector 4.0.1 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/backend/nitro.json +1 -1
- package/.output/cli.js +193 -110
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-CTHqmroT.mjs → router-ChILzhmz.mjs} +92 -4
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-Co3cUF_I.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-jFcrL37X.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-BPzt_VpM.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-C6O_zuwI.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-Ca6NvB7L.js} +1 -1
- package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BzqKcveJ.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-9pCZIsS_.js} +1 -1
- package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-BMP3KVGR.js} +1 -1
- package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-y4eRoT1H.js} +1 -1
- package/.output/ui/assets/{index-Ea-c8hPg.js → index-CH5w-haG.js} +1 -1
- package/.output/ui/assets/{index-zVVS53Hd.js → index-CmocEhyU.js} +1 -1
- package/.output/ui/assets/{index-Du4qNOOt.js → index-DT8sMJ_n.js} +2 -2
- package/.output/ui/assets/{index-gvFj_9K6.js → index-bfsbIRZa.js} +1 -1
- package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-BfFOSDG5.js} +1 -1
- package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkbVkOsp.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +5 -10
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1433
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -258
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { JSX, KeyboardEvent as ReactKeyboardEvent } from "react";
|
|
3
|
-
import {
|
|
4
|
-
Check,
|
|
5
|
-
ChevronRight,
|
|
6
|
-
Columns2,
|
|
7
|
-
Copy,
|
|
8
|
-
Equal,
|
|
9
|
-
Minus,
|
|
10
|
-
Pencil,
|
|
11
|
-
Plus,
|
|
12
|
-
Rows3,
|
|
13
|
-
X,
|
|
14
|
-
} from "lucide-react";
|
|
15
|
-
import { cn, formatTokens } from "../../lib/utils";
|
|
16
|
-
import { copyTextToClipboard } from "../../lib/clipboard";
|
|
17
|
-
import type { CapturedLog } from "../../contracts";
|
|
18
|
-
import {
|
|
19
|
-
type DiffOp,
|
|
20
|
-
type JsonNode,
|
|
21
|
-
diffTrees,
|
|
22
|
-
normalizeRequest,
|
|
23
|
-
previewNode,
|
|
24
|
-
} from "./requestDiff";
|
|
25
|
-
import { getConversationId } from "./ConversationHeader";
|
|
26
|
-
import { Badge } from "../ui/badge";
|
|
27
|
-
import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
|
|
28
|
-
import { LazyJsonViewerFromString } from "./lazy";
|
|
29
|
-
|
|
30
|
-
export type CompareDrawerProps = {
|
|
31
|
-
/** Log selected first (shown on the left). */
|
|
32
|
-
left: CapturedLog;
|
|
33
|
-
/** Log selected second (shown on the right). */
|
|
34
|
-
right: CapturedLog;
|
|
35
|
-
/** Human-facing ordinal inside the left log's session. */
|
|
36
|
-
leftDisplayNumber: number;
|
|
37
|
-
/** Human-facing ordinal inside the right log's session. */
|
|
38
|
-
rightDisplayNumber: number;
|
|
39
|
-
onClose: () => void;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type EqualOp = Extract<DiffOp, { kind: "equal" }>;
|
|
43
|
-
type AddedOp = Extract<DiffOp, { kind: "added" }>;
|
|
44
|
-
type RemovedOp = Extract<DiffOp, { kind: "removed" }>;
|
|
45
|
-
type ChangedOp = Extract<DiffOp, { kind: "changed" }>;
|
|
46
|
-
|
|
47
|
-
type DiffMode = "unified" | "split";
|
|
48
|
-
|
|
49
|
-
export const COMPARE_RESPONSIVE_HEADER_CLASS_NAME =
|
|
50
|
-
"flex flex-col gap-3 border-b border-border px-3 py-3 sm:flex-row sm:items-start sm:gap-4 sm:px-4";
|
|
51
|
-
export const COMPARE_RESPONSIVE_SPLIT_CLASS_NAME =
|
|
52
|
-
"grid min-w-0 grid-cols-1 gap-x-2 gap-y-0.5 px-3 py-2 text-xs md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]";
|
|
53
|
-
|
|
54
|
-
/** Walk the JsonNode tree and pretty-print it back to a JSON string for the
|
|
55
|
-
* expanded-equal-subtree view. The node is a plain object structure so
|
|
56
|
-
* `JSON.stringify` produces correct output. */
|
|
57
|
-
function nodeToJsonString(node: JsonNode, indent = 2): string {
|
|
58
|
-
return JSON.stringify(nodeToJsonValue(node), null, indent);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function nodeToJsonValue(node: JsonNode): unknown {
|
|
62
|
-
switch (node.kind) {
|
|
63
|
-
case "primitive":
|
|
64
|
-
return node.value;
|
|
65
|
-
case "array":
|
|
66
|
-
return node.value.map(nodeToJsonValue);
|
|
67
|
-
case "object": {
|
|
68
|
-
const out: Record<string, unknown> = {};
|
|
69
|
-
for (const [k, v] of Object.entries(node.value)) {
|
|
70
|
-
out[k] = nodeToJsonValue(v);
|
|
71
|
-
}
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function LazyJsonStringView({
|
|
78
|
-
text,
|
|
79
|
-
defaultExpandDepth,
|
|
80
|
-
}: {
|
|
81
|
-
text: string;
|
|
82
|
-
defaultExpandDepth: number;
|
|
83
|
-
}): JSX.Element {
|
|
84
|
-
return (
|
|
85
|
-
<Suspense fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}>
|
|
86
|
-
<LazyJsonViewerFromString text={text} defaultExpandDepth={defaultExpandDepth} />
|
|
87
|
-
</Suspense>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** The parent path of a JSON path string. E.g. `messages[3].content` →
|
|
92
|
-
* `messages[3]`, `messages[3]` → `messages`, `messages` → `""`. */
|
|
93
|
-
function parentPath(path: string): string {
|
|
94
|
-
if (path === "") return "";
|
|
95
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
96
|
-
const ch = path[i];
|
|
97
|
-
if (ch === "." || ch === "[") {
|
|
98
|
-
return path.substring(0, i);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return "";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Group contiguous deep-equal ops (object/array, not primitive) that share
|
|
105
|
-
* a common parent into a single collapsed row, so an unchanged block of N
|
|
106
|
-
* sibling subtrees renders as one row instead of N. Primitive equals
|
|
107
|
-
* always render as their own row (they're 1 line each). */
|
|
108
|
-
type GroupedOp = { kind: "single"; op: DiffOp } | { kind: "equal-run"; ops: EqualOp[] };
|
|
109
|
-
|
|
110
|
-
function isDeepEqual(op: DiffOp): op is EqualOp {
|
|
111
|
-
return op.kind === "equal" && (op.value.kind === "object" || op.value.kind === "array");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function groupContiguousEquals(ops: DiffOp[]): GroupedOp[] {
|
|
115
|
-
const out: GroupedOp[] = [];
|
|
116
|
-
let i = 0;
|
|
117
|
-
while (i < ops.length) {
|
|
118
|
-
const op = ops[i];
|
|
119
|
-
if (op !== undefined && isDeepEqual(op)) {
|
|
120
|
-
const startParent = parentPath(op.path);
|
|
121
|
-
let j = i + 1;
|
|
122
|
-
while (j < ops.length) {
|
|
123
|
-
const next = ops[j];
|
|
124
|
-
if (next === undefined) break;
|
|
125
|
-
if (!isDeepEqual(next)) break;
|
|
126
|
-
if (parentPath(next.path) !== startParent) break;
|
|
127
|
-
j++;
|
|
128
|
-
}
|
|
129
|
-
if (j - i > 1) {
|
|
130
|
-
const equalOps: EqualOp[] = [];
|
|
131
|
-
for (let k = i; k < j; k++) {
|
|
132
|
-
const eop = ops[k];
|
|
133
|
-
if (eop !== undefined && eop.kind === "equal") {
|
|
134
|
-
equalOps.push(eop);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
out.push({ kind: "equal-run", ops: equalOps });
|
|
138
|
-
i = j;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (op !== undefined) {
|
|
143
|
-
out.push({ kind: "single", op });
|
|
144
|
-
}
|
|
145
|
-
i++;
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Visual configuration for each diff kind. Centralized so the unified row,
|
|
151
|
-
* summary chips, and split columns all use the same colors / icons. */
|
|
152
|
-
type DiffKind = "added" | "removed" | "changed" | "equal";
|
|
153
|
-
|
|
154
|
-
const KIND_VISUAL: Record<
|
|
155
|
-
DiffKind,
|
|
156
|
-
{
|
|
157
|
-
icon: typeof Plus;
|
|
158
|
-
/** Accent color for the icon + label. */
|
|
159
|
-
accent: string;
|
|
160
|
-
/** Background tint applied to the row. */
|
|
161
|
-
bg: string;
|
|
162
|
-
/** Thick left-border color. */
|
|
163
|
-
border: string;
|
|
164
|
-
/** Short text label (uppercase, used in the row badge). */
|
|
165
|
-
label: string;
|
|
166
|
-
}
|
|
167
|
-
> = {
|
|
168
|
-
added: {
|
|
169
|
-
icon: Plus,
|
|
170
|
-
accent: "text-status-success",
|
|
171
|
-
bg: "bg-status-success/5 hover:bg-status-success/10",
|
|
172
|
-
border: "border-l-status-success",
|
|
173
|
-
label: "ADDED",
|
|
174
|
-
},
|
|
175
|
-
removed: {
|
|
176
|
-
icon: Minus,
|
|
177
|
-
accent: "text-status-danger",
|
|
178
|
-
bg: "bg-status-danger/5 hover:bg-status-danger/10",
|
|
179
|
-
border: "border-l-status-danger",
|
|
180
|
-
label: "REMOVED",
|
|
181
|
-
},
|
|
182
|
-
changed: {
|
|
183
|
-
icon: Pencil,
|
|
184
|
-
accent: "text-status-watch",
|
|
185
|
-
bg: "bg-status-watch/5 hover:bg-status-watch/10",
|
|
186
|
-
border: "border-l-status-watch",
|
|
187
|
-
label: "CHANGED",
|
|
188
|
-
},
|
|
189
|
-
equal: {
|
|
190
|
-
icon: Equal,
|
|
191
|
-
accent: "text-muted-foreground/70",
|
|
192
|
-
bg: "bg-muted/20 hover:bg-muted/30",
|
|
193
|
-
border: "border-l-muted-foreground/20",
|
|
194
|
-
label: "EQUAL",
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
function EqualRunRow({
|
|
199
|
-
ops,
|
|
200
|
-
expanded,
|
|
201
|
-
onToggle,
|
|
202
|
-
}: {
|
|
203
|
-
ops: EqualOp[];
|
|
204
|
-
expanded: boolean;
|
|
205
|
-
onToggle: () => void;
|
|
206
|
-
}): JSX.Element {
|
|
207
|
-
const first = ops[0];
|
|
208
|
-
const last = ops[ops.length - 1];
|
|
209
|
-
if (first === undefined || last === undefined) {
|
|
210
|
-
return <div className="text-muted-foreground/40 text-xs">—</div>;
|
|
211
|
-
}
|
|
212
|
-
const firstPath = first.path;
|
|
213
|
-
const lastPath = last.path;
|
|
214
|
-
const label = ops.length === 1 ? firstPath : `${firstPath} … ${lastPath}`;
|
|
215
|
-
const summary =
|
|
216
|
-
first.value.kind === "array"
|
|
217
|
-
? `${ops.length} equal arrays`
|
|
218
|
-
: first.value.kind === "object"
|
|
219
|
-
? `${ops.length} equal objects`
|
|
220
|
-
: "equal";
|
|
221
|
-
|
|
222
|
-
const v = KIND_VISUAL.equal;
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<div className={cn("border-l-4 rounded-sm", v.border, v.bg)}>
|
|
226
|
-
<button
|
|
227
|
-
type="button"
|
|
228
|
-
onClick={onToggle}
|
|
229
|
-
className="w-full text-left flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground cursor-pointer"
|
|
230
|
-
>
|
|
231
|
-
<ChevronRight
|
|
232
|
-
className={cn("size-3 transition-transform shrink-0", expanded && "rotate-90")}
|
|
233
|
-
/>
|
|
234
|
-
<v.icon className={cn("size-3 shrink-0", v.accent)} />
|
|
235
|
-
<span className="font-mono truncate flex-1" title={`${firstPath} … ${lastPath}`}>
|
|
236
|
-
{label}
|
|
237
|
-
</span>
|
|
238
|
-
<span className={cn("text-[10px] uppercase tracking-wider shrink-0", v.accent)}>
|
|
239
|
-
{v.label}
|
|
240
|
-
</span>
|
|
241
|
-
<span className="text-muted-foreground/60 shrink-0">({summary})</span>
|
|
242
|
-
</button>
|
|
243
|
-
{expanded && (
|
|
244
|
-
<div className="ml-5 mt-1 mb-2 space-y-2 pr-2">
|
|
245
|
-
{ops.map((op) => (
|
|
246
|
-
<div key={op.path} className="border border-border/50 rounded p-2 bg-muted/20">
|
|
247
|
-
<div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
|
|
248
|
-
<LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
</div>
|
|
252
|
-
)}
|
|
253
|
-
</div>
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** A single diff row in the unified view. Renders the icon, path, kind label,
|
|
258
|
-
* value preview, and a copy-path button. `data-diff-idx` lets the parent
|
|
259
|
-
* scroll the row into view when a summary chip is clicked.
|
|
260
|
-
*
|
|
261
|
-
* When `expanded` is true (controlled by the parent), renders the full
|
|
262
|
-
* added / removed / changed subtree below the row. Primitive changes and
|
|
263
|
-
* primitive equal values are not expandable (the value is already fully
|
|
264
|
-
* shown on the row). */
|
|
265
|
-
function UnifiedOpRow({
|
|
266
|
-
op,
|
|
267
|
-
idx,
|
|
268
|
-
copiedPath,
|
|
269
|
-
onCopyPath,
|
|
270
|
-
expanded,
|
|
271
|
-
onToggle,
|
|
272
|
-
}: {
|
|
273
|
-
op: AddedOp | RemovedOp | ChangedOp | EqualOp;
|
|
274
|
-
idx: number;
|
|
275
|
-
copiedPath: string | null;
|
|
276
|
-
onCopyPath: (path: string) => void;
|
|
277
|
-
expanded: boolean;
|
|
278
|
-
onToggle: () => void;
|
|
279
|
-
}): JSX.Element {
|
|
280
|
-
const v = KIND_VISUAL[op.kind];
|
|
281
|
-
const Icon = v.icon;
|
|
282
|
-
|
|
283
|
-
// True iff this row has a useful subtree to drill into. Primitives (and
|
|
284
|
-
// the root "equal" pseudo-op) have no extra detail beyond the preview.
|
|
285
|
-
const isExpandable =
|
|
286
|
-
op.kind === "added" || op.kind === "removed"
|
|
287
|
-
? op.value.kind === "object" || op.value.kind === "array"
|
|
288
|
-
: op.kind === "changed"
|
|
289
|
-
? op.left.kind === "object" ||
|
|
290
|
-
op.left.kind === "array" ||
|
|
291
|
-
op.right.kind === "object" ||
|
|
292
|
-
op.right.kind === "array"
|
|
293
|
-
: false; // equal (single primitive) — preview is the whole value
|
|
294
|
-
|
|
295
|
-
// For changed rows, show the left and right previews stacked. For added /
|
|
296
|
-
// removed / equal, one preview is enough.
|
|
297
|
-
const preview =
|
|
298
|
-
op.kind === "changed"
|
|
299
|
-
? [
|
|
300
|
-
{
|
|
301
|
-
text: previewNode(op.left, 400),
|
|
302
|
-
tone: "text-status-danger line-through",
|
|
303
|
-
},
|
|
304
|
-
{ text: previewNode(op.right, 400), tone: "text-status-success" },
|
|
305
|
-
]
|
|
306
|
-
: op.kind === "removed"
|
|
307
|
-
? [
|
|
308
|
-
{
|
|
309
|
-
text: previewNode(op.value, 400),
|
|
310
|
-
tone: "text-status-danger line-through",
|
|
311
|
-
},
|
|
312
|
-
]
|
|
313
|
-
: op.kind === "added"
|
|
314
|
-
? [{ text: previewNode(op.value, 400), tone: "text-status-success" }]
|
|
315
|
-
: [{ text: previewNode(op.value, 400), tone: "text-muted-foreground" }];
|
|
316
|
-
|
|
317
|
-
const justCopied = copiedPath === op.path && op.path !== "";
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<div
|
|
321
|
-
data-diff-idx={idx}
|
|
322
|
-
data-diff-kind={op.kind}
|
|
323
|
-
className={cn("border-l-4 rounded-sm px-3 py-2 my-0.5 transition-colors", v.border, v.bg)}
|
|
324
|
-
>
|
|
325
|
-
<button
|
|
326
|
-
type="button"
|
|
327
|
-
onClick={onToggle}
|
|
328
|
-
disabled={!isExpandable}
|
|
329
|
-
className={cn(
|
|
330
|
-
"w-full flex items-center gap-2 text-xs text-left rounded-sm",
|
|
331
|
-
isExpandable ? "cursor-pointer" : "cursor-default",
|
|
332
|
-
)}
|
|
333
|
-
aria-expanded={isExpandable ? expanded : undefined}
|
|
334
|
-
aria-label={
|
|
335
|
-
isExpandable
|
|
336
|
-
? expanded
|
|
337
|
-
? `Collapse ${op.path || "root"}`
|
|
338
|
-
: `Expand ${op.path || "root"}`
|
|
339
|
-
: undefined
|
|
340
|
-
}
|
|
341
|
-
>
|
|
342
|
-
{isExpandable ? (
|
|
343
|
-
<ChevronRight
|
|
344
|
-
className={cn(
|
|
345
|
-
"size-3 shrink-0 transition-transform",
|
|
346
|
-
v.accent,
|
|
347
|
-
expanded && "rotate-90",
|
|
348
|
-
)}
|
|
349
|
-
/>
|
|
350
|
-
) : (
|
|
351
|
-
<span className="size-3 shrink-0" aria-hidden="true" />
|
|
352
|
-
)}
|
|
353
|
-
<Icon className={cn("size-3.5 shrink-0", v.accent)} strokeWidth={2.5} />
|
|
354
|
-
<span className="font-mono truncate flex-1 min-w-0" title={op.path || "(root)"}>
|
|
355
|
-
{op.path === "" ? "(root)" : op.path}
|
|
356
|
-
</span>
|
|
357
|
-
<span
|
|
358
|
-
className={cn(
|
|
359
|
-
"text-[9px] font-bold uppercase tracking-wider shrink-0 px-1.5 py-0.5 rounded",
|
|
360
|
-
v.accent,
|
|
361
|
-
op.kind === "equal" ? "bg-muted/40" : "bg-background/60",
|
|
362
|
-
)}
|
|
363
|
-
>
|
|
364
|
-
{v.label}
|
|
365
|
-
</span>
|
|
366
|
-
{op.path !== "" && (
|
|
367
|
-
<span
|
|
368
|
-
role="button"
|
|
369
|
-
tabIndex={0}
|
|
370
|
-
onClick={(e) => {
|
|
371
|
-
e.stopPropagation();
|
|
372
|
-
onCopyPath(op.path);
|
|
373
|
-
}}
|
|
374
|
-
onKeyDown={(e) => {
|
|
375
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
376
|
-
e.stopPropagation();
|
|
377
|
-
e.preventDefault();
|
|
378
|
-
onCopyPath(op.path);
|
|
379
|
-
}
|
|
380
|
-
}}
|
|
381
|
-
className={cn(
|
|
382
|
-
"shrink-0 p-1 rounded transition-colors cursor-pointer inline-flex items-center justify-center",
|
|
383
|
-
justCopied
|
|
384
|
-
? "text-status-success"
|
|
385
|
-
: "text-muted-foreground/50 hover:text-foreground hover:bg-muted",
|
|
386
|
-
)}
|
|
387
|
-
aria-label={justCopied ? "Copied" : "Copy"}
|
|
388
|
-
title={justCopied ? "Copied!" : "Copy"}
|
|
389
|
-
>
|
|
390
|
-
{justCopied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
|
391
|
-
</span>
|
|
392
|
-
)}
|
|
393
|
-
</button>
|
|
394
|
-
{preview.map((p, i) => (
|
|
395
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: preview list is rebuilt on every render and is positional
|
|
396
|
-
<div key={i} className={cn("font-mono text-xs mt-1 break-all pl-5", p.tone)}>
|
|
397
|
-
{p.text}
|
|
398
|
-
</div>
|
|
399
|
-
))}
|
|
400
|
-
<div
|
|
401
|
-
className="overflow-hidden transition-all duration-200"
|
|
402
|
-
style={{ maxHeight: expanded && isExpandable ? "2000px" : "0" }}
|
|
403
|
-
aria-hidden={!expanded}
|
|
404
|
-
>
|
|
405
|
-
{expanded && isExpandable && op.kind !== "equal" ? <ExpandedSubtree op={op} /> : null}
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** Render the full subtree for an expanded diff row.
|
|
412
|
-
* - added / removed: a single JsonViewerFromString for the value.
|
|
413
|
-
* - changed (object/array on either side): two viewers side by side.
|
|
414
|
-
* - changed (both primitives): a small "values are shown above" note. */
|
|
415
|
-
function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.Element {
|
|
416
|
-
if (op.kind === "added" || op.kind === "removed") {
|
|
417
|
-
return (
|
|
418
|
-
<div className="pl-5 mt-2 border border-border/50 rounded p-2 bg-muted/20">
|
|
419
|
-
<LazyJsonStringView text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
|
|
420
|
-
</div>
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
// changed
|
|
424
|
-
const leftIsStructured = op.left.kind === "object" || op.left.kind === "array";
|
|
425
|
-
const rightIsStructured = op.right.kind === "object" || op.right.kind === "array";
|
|
426
|
-
if (!leftIsStructured && !rightIsStructured) {
|
|
427
|
-
return (
|
|
428
|
-
<div className="pl-5 mt-2 text-xs text-muted-foreground/70 italic">
|
|
429
|
-
Primitive values are shown inline above.
|
|
430
|
-
</div>
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
return (
|
|
434
|
-
<div className="pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
435
|
-
<div className="rounded border border-status-danger/30 bg-status-danger/5 p-2">
|
|
436
|
-
<div className="mb-1 text-[10px] uppercase tracking-wider text-status-danger">Old</div>
|
|
437
|
-
<LazyJsonStringView text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
|
|
438
|
-
</div>
|
|
439
|
-
<div className="rounded border border-status-success/30 bg-status-success/5 p-2">
|
|
440
|
-
<div className="mb-1 text-[10px] uppercase tracking-wider text-status-success">New</div>
|
|
441
|
-
<LazyJsonStringView text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
|
|
442
|
-
</div>
|
|
443
|
-
</div>
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** Summary chips at the top of the body. Each chip is a button that scrolls
|
|
448
|
-
* the body to the first op of that kind. Disabled when the count is 0. */
|
|
449
|
-
function SummaryChips({
|
|
450
|
-
counts,
|
|
451
|
-
onJumpTo,
|
|
452
|
-
}: {
|
|
453
|
-
counts: { added: number; removed: number; changed: number };
|
|
454
|
-
onJumpTo: (kind: "added" | "removed" | "changed") => void;
|
|
455
|
-
}): JSX.Element {
|
|
456
|
-
const total = counts.added + counts.removed + counts.changed;
|
|
457
|
-
return (
|
|
458
|
-
<div className="px-4 py-2 border-b border-border bg-muted/20 flex items-center gap-2 text-xs flex-wrap">
|
|
459
|
-
<span className="text-muted-foreground font-medium">
|
|
460
|
-
{total} {total === 1 ? "change" : "changes"}
|
|
461
|
-
</span>
|
|
462
|
-
<button
|
|
463
|
-
type="button"
|
|
464
|
-
onClick={() => onJumpTo("removed")}
|
|
465
|
-
disabled={counts.removed === 0}
|
|
466
|
-
className={cn(
|
|
467
|
-
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
|
|
468
|
-
counts.removed > 0
|
|
469
|
-
? "border-status-danger/40 bg-status-danger/10 text-status-danger hover:bg-status-danger/20"
|
|
470
|
-
: "border-border text-muted-foreground/40 cursor-not-allowed",
|
|
471
|
-
)}
|
|
472
|
-
title={counts.removed > 0 ? "Jump to first removed" : "No removals"}
|
|
473
|
-
>
|
|
474
|
-
<Minus className="size-3" />
|
|
475
|
-
{counts.removed} removed
|
|
476
|
-
</button>
|
|
477
|
-
<button
|
|
478
|
-
type="button"
|
|
479
|
-
onClick={() => onJumpTo("added")}
|
|
480
|
-
disabled={counts.added === 0}
|
|
481
|
-
className={cn(
|
|
482
|
-
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
|
|
483
|
-
counts.added > 0
|
|
484
|
-
? "border-status-success/40 bg-status-success/10 text-status-success hover:bg-status-success/20"
|
|
485
|
-
: "border-border text-muted-foreground/40 cursor-not-allowed",
|
|
486
|
-
)}
|
|
487
|
-
title={counts.added > 0 ? "Jump to first added" : "No additions"}
|
|
488
|
-
>
|
|
489
|
-
<Plus className="size-3" />
|
|
490
|
-
{counts.added} added
|
|
491
|
-
</button>
|
|
492
|
-
<button
|
|
493
|
-
type="button"
|
|
494
|
-
onClick={() => onJumpTo("changed")}
|
|
495
|
-
disabled={counts.changed === 0}
|
|
496
|
-
className={cn(
|
|
497
|
-
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
|
|
498
|
-
counts.changed > 0
|
|
499
|
-
? "border-status-watch/40 bg-status-watch/10 text-status-watch hover:bg-status-watch/20"
|
|
500
|
-
: "border-border text-muted-foreground/40 cursor-not-allowed",
|
|
501
|
-
)}
|
|
502
|
-
title={counts.changed > 0 ? "Jump to first changed" : "No changes"}
|
|
503
|
-
>
|
|
504
|
-
<Pencil className="size-3" />
|
|
505
|
-
{counts.changed} changed
|
|
506
|
-
</button>
|
|
507
|
-
</div>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** Mode toggle in the header: unified (single column) or split (path | left | right). */
|
|
512
|
-
function ModeToggle({
|
|
513
|
-
mode,
|
|
514
|
-
onChange,
|
|
515
|
-
}: {
|
|
516
|
-
mode: DiffMode;
|
|
517
|
-
onChange: (mode: DiffMode) => void;
|
|
518
|
-
}): JSX.Element {
|
|
519
|
-
return (
|
|
520
|
-
<div className="inline-flex rounded-md border border-border overflow-hidden">
|
|
521
|
-
<button
|
|
522
|
-
type="button"
|
|
523
|
-
onClick={() => onChange("unified")}
|
|
524
|
-
aria-pressed={mode === "unified"}
|
|
525
|
-
className={cn(
|
|
526
|
-
"flex items-center gap-1 px-2 py-1 text-xs transition-colors cursor-pointer",
|
|
527
|
-
mode === "unified"
|
|
528
|
-
? "bg-muted text-foreground"
|
|
529
|
-
: "hover:bg-muted/50 text-muted-foreground",
|
|
530
|
-
)}
|
|
531
|
-
title="Unified view (single column, emphasized diffs)"
|
|
532
|
-
>
|
|
533
|
-
<Rows3 className="size-3" />
|
|
534
|
-
Unified
|
|
535
|
-
</button>
|
|
536
|
-
<button
|
|
537
|
-
type="button"
|
|
538
|
-
onClick={() => onChange("split")}
|
|
539
|
-
aria-pressed={mode === "split"}
|
|
540
|
-
className={cn(
|
|
541
|
-
"flex items-center gap-1 px-2 py-1 text-xs transition-colors border-l border-border cursor-pointer",
|
|
542
|
-
mode === "split" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground",
|
|
543
|
-
)}
|
|
544
|
-
title="Split view (path | left | right)"
|
|
545
|
-
>
|
|
546
|
-
<Columns2 className="size-3" />
|
|
547
|
-
Split
|
|
548
|
-
</button>
|
|
549
|
-
</div>
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function SideSummary({
|
|
554
|
-
log,
|
|
555
|
-
side,
|
|
556
|
-
displayNumber,
|
|
557
|
-
}: {
|
|
558
|
-
log: CapturedLog;
|
|
559
|
-
side: "left" | "right";
|
|
560
|
-
displayNumber: number;
|
|
561
|
-
}): JSX.Element {
|
|
562
|
-
const conversationId = getConversationId(log);
|
|
563
|
-
return (
|
|
564
|
-
<div className="flex-1 min-w-0 space-y-1 text-xs">
|
|
565
|
-
<div className="flex items-center gap-2">
|
|
566
|
-
<Badge
|
|
567
|
-
variant="outline"
|
|
568
|
-
className={cn(
|
|
569
|
-
"text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
|
|
570
|
-
side === "left"
|
|
571
|
-
? "border-status-danger/40 text-status-danger"
|
|
572
|
-
: "border-status-success/40 text-status-success",
|
|
573
|
-
)}
|
|
574
|
-
>
|
|
575
|
-
{side === "left" ? "← Left" : "Right →"}
|
|
576
|
-
</Badge>
|
|
577
|
-
<span className="font-mono text-status-info" title={`Log ID ${String(log.id)}`}>
|
|
578
|
-
#{displayNumber}
|
|
579
|
-
</span>
|
|
580
|
-
{log.model !== null && (
|
|
581
|
-
<span className="min-w-0 truncate font-mono leading-snug text-muted-foreground">
|
|
582
|
-
{log.model}
|
|
583
|
-
</span>
|
|
584
|
-
)}
|
|
585
|
-
</div>
|
|
586
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 font-mono text-muted-foreground">
|
|
587
|
-
{log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
|
|
588
|
-
<span className="text-status-success">
|
|
589
|
-
KV Cache +{formatTokens(log.cacheCreationInputTokens)}
|
|
590
|
-
</span>
|
|
591
|
-
)}
|
|
592
|
-
{log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
|
|
593
|
-
<span className="text-status-accent">
|
|
594
|
-
KV Cache ~{formatTokens(log.cacheReadInputTokens)}
|
|
595
|
-
</span>
|
|
596
|
-
)}
|
|
597
|
-
<span className="truncate" title={log.timestamp}>
|
|
598
|
-
{log.timestamp}
|
|
599
|
-
</span>
|
|
600
|
-
</div>
|
|
601
|
-
<div className="text-muted-foreground/70 font-mono truncate" title={conversationId}>
|
|
602
|
-
session: {conversationId}
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
export function CompareDrawer({
|
|
609
|
-
left,
|
|
610
|
-
right,
|
|
611
|
-
leftDisplayNumber,
|
|
612
|
-
rightDisplayNumber,
|
|
613
|
-
onClose,
|
|
614
|
-
}: CompareDrawerProps): JSX.Element {
|
|
615
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
616
|
-
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
|
617
|
-
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
|
618
|
-
// Memoize the diff so re-renders (e.g. parent re-renders) don't recompute.
|
|
619
|
-
const ops = useMemo<DiffOp[]>(() => {
|
|
620
|
-
const leftRequest = getLogFormatAdapter(resolveLogFormat(left)).analyzeRequest(
|
|
621
|
-
left.rawRequestBody,
|
|
622
|
-
);
|
|
623
|
-
const rightRequest = getLogFormatAdapter(resolveLogFormat(right)).analyzeRequest(
|
|
624
|
-
right.rawRequestBody,
|
|
625
|
-
);
|
|
626
|
-
const l = normalizeRequest(leftRequest.comparisonValue);
|
|
627
|
-
const r = normalizeRequest(rightRequest.comparisonValue);
|
|
628
|
-
return diffTrees(l, r);
|
|
629
|
-
}, [
|
|
630
|
-
left.apiFormat,
|
|
631
|
-
left.path,
|
|
632
|
-
left.rawRequestBody,
|
|
633
|
-
right.apiFormat,
|
|
634
|
-
right.path,
|
|
635
|
-
right.rawRequestBody,
|
|
636
|
-
]);
|
|
637
|
-
|
|
638
|
-
const grouped = useMemo(() => groupContiguousEquals(ops), [ops]);
|
|
639
|
-
|
|
640
|
-
// Count diff ops by kind (ignoring equal-run groups, which are always
|
|
641
|
-
// collapsed and not actionable). Drives the summary chips.
|
|
642
|
-
const counts = useMemo(() => {
|
|
643
|
-
let added = 0;
|
|
644
|
-
let removed = 0;
|
|
645
|
-
let changed = 0;
|
|
646
|
-
for (const g of grouped) {
|
|
647
|
-
if (g.kind !== "single") continue;
|
|
648
|
-
switch (g.op.kind) {
|
|
649
|
-
case "added":
|
|
650
|
-
added++;
|
|
651
|
-
break;
|
|
652
|
-
case "removed":
|
|
653
|
-
removed++;
|
|
654
|
-
break;
|
|
655
|
-
case "changed":
|
|
656
|
-
changed++;
|
|
657
|
-
break;
|
|
658
|
-
case "equal":
|
|
659
|
-
break;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
return { added, removed, changed };
|
|
663
|
-
}, [grouped]);
|
|
664
|
-
|
|
665
|
-
// Track which collapsed equal runs are expanded.
|
|
666
|
-
const [expandedRuns, setExpandedRuns] = useState<Set<number>>(new Set());
|
|
667
|
-
const toggleRun = (idx: number) => {
|
|
668
|
-
setExpandedRuns((prev) => {
|
|
669
|
-
const next = new Set(prev);
|
|
670
|
-
if (next.has(idx)) next.delete(idx);
|
|
671
|
-
else next.add(idx);
|
|
672
|
-
return next;
|
|
673
|
-
});
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// Track which individual diff rows (added / removed / changed) are
|
|
677
|
-
// expanded. Independent of `expandedRuns` (which is for equal-run groups)
|
|
678
|
-
// so the two expand affordances don't interfere.
|
|
679
|
-
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
680
|
-
const toggleRow = (idx: number) => {
|
|
681
|
-
setExpandedRows((prev) => {
|
|
682
|
-
const next = new Set(prev);
|
|
683
|
-
if (next.has(idx)) next.delete(idx);
|
|
684
|
-
else next.add(idx);
|
|
685
|
-
return next;
|
|
686
|
-
});
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
// Reset per-row expand state when the comparison pair changes.
|
|
690
|
-
useEffect(() => {
|
|
691
|
-
setExpandedRows(new Set());
|
|
692
|
-
}, [left.id, right.id]);
|
|
693
|
-
|
|
694
|
-
const [mode, setMode] = useState<DiffMode>("unified");
|
|
695
|
-
|
|
696
|
-
// Body scroll container — used to scroll a target row into view when a
|
|
697
|
-
// summary chip is clicked.
|
|
698
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
699
|
-
|
|
700
|
-
// Most-recently-copied path. Used to flash the icon from Copy → Check.
|
|
701
|
-
const [copiedPath, setCopiedPath] = useState<string | null>(null);
|
|
702
|
-
const copyResetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
703
|
-
const onCopyPath = (path: string) => {
|
|
704
|
-
void copyTextToClipboard(path).then((success) => {
|
|
705
|
-
if (!success) return;
|
|
706
|
-
setCopiedPath(path);
|
|
707
|
-
if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
|
|
708
|
-
copyResetTimer.current = setTimeout(() => setCopiedPath(null), 1500);
|
|
709
|
-
});
|
|
710
|
-
};
|
|
711
|
-
useEffect(() => {
|
|
712
|
-
return () => {
|
|
713
|
-
if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
|
|
714
|
-
};
|
|
715
|
-
}, []);
|
|
716
|
-
|
|
717
|
-
// Find the grouped-index of the first op matching `kind`, and scroll to it.
|
|
718
|
-
const jumpToKind = (kind: "added" | "removed" | "changed") => {
|
|
719
|
-
const idx = grouped.findIndex((g) => g.kind === "single" && g.op.kind === kind);
|
|
720
|
-
if (idx === -1) return;
|
|
721
|
-
const root = bodyRef.current;
|
|
722
|
-
if (root === null) return;
|
|
723
|
-
const el = root.querySelector(`[data-diff-idx="${idx}"]`);
|
|
724
|
-
if (el !== null) {
|
|
725
|
-
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
726
|
-
}
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
// Esc keybinding, initial focus, focus restoration, and body scroll lock while open.
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
previousActiveElementRef.current =
|
|
732
|
-
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
733
|
-
closeButtonRef.current?.focus();
|
|
734
|
-
|
|
735
|
-
const onKey = (e: KeyboardEvent) => {
|
|
736
|
-
if (e.key === "Escape") onClose();
|
|
737
|
-
};
|
|
738
|
-
document.addEventListener("keydown", onKey);
|
|
739
|
-
const prevOverflow = document.body.style.overflow;
|
|
740
|
-
document.body.style.overflow = "hidden";
|
|
741
|
-
return () => {
|
|
742
|
-
document.removeEventListener("keydown", onKey);
|
|
743
|
-
document.body.style.overflow = prevOverflow;
|
|
744
|
-
previousActiveElementRef.current?.focus();
|
|
745
|
-
};
|
|
746
|
-
}, [onClose]);
|
|
747
|
-
|
|
748
|
-
const handlePanelKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>): void => {
|
|
749
|
-
event.stopPropagation();
|
|
750
|
-
if (event.key !== "Tab") return;
|
|
751
|
-
|
|
752
|
-
const panel = panelRef.current;
|
|
753
|
-
if (panel === null) return;
|
|
754
|
-
const focusable = Array.from(
|
|
755
|
-
panel.querySelectorAll<HTMLElement>(
|
|
756
|
-
'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
757
|
-
),
|
|
758
|
-
);
|
|
759
|
-
const first = focusable[0];
|
|
760
|
-
const last = focusable[focusable.length - 1];
|
|
761
|
-
if (first === undefined || last === undefined) return;
|
|
762
|
-
|
|
763
|
-
if (event.shiftKey && document.activeElement === first) {
|
|
764
|
-
event.preventDefault();
|
|
765
|
-
last.focus();
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
if (!event.shiftKey && document.activeElement === last) {
|
|
769
|
-
event.preventDefault();
|
|
770
|
-
first.focus();
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const sameSession = getConversationId(left) === getConversationId(right);
|
|
775
|
-
const allEqual = ops.length === 1 && ops[0]?.kind === "equal";
|
|
776
|
-
|
|
777
|
-
return (
|
|
778
|
-
<div
|
|
779
|
-
className="fixed inset-0 z-50 flex justify-end"
|
|
780
|
-
role="dialog"
|
|
781
|
-
aria-modal="true"
|
|
782
|
-
aria-label="Compare two log requests"
|
|
783
|
-
>
|
|
784
|
-
{/* Backdrop */}
|
|
785
|
-
<button
|
|
786
|
-
type="button"
|
|
787
|
-
onClick={onClose}
|
|
788
|
-
aria-label="Close compare drawer"
|
|
789
|
-
className="absolute inset-0 bg-black/40 cursor-default"
|
|
790
|
-
tabIndex={-1}
|
|
791
|
-
/>
|
|
792
|
-
|
|
793
|
-
{/* Drawer panel */}
|
|
794
|
-
<div
|
|
795
|
-
ref={panelRef}
|
|
796
|
-
className={cn(
|
|
797
|
-
"relative overflow-x-hidden bg-background border-l border-border shadow-xl",
|
|
798
|
-
"w-full md:w-[70vw] max-w-[1100px] flex flex-col h-full",
|
|
799
|
-
)}
|
|
800
|
-
onClick={(e) => e.stopPropagation()}
|
|
801
|
-
onKeyDown={handlePanelKeyDown}
|
|
802
|
-
>
|
|
803
|
-
{/* Header */}
|
|
804
|
-
<div className={COMPARE_RESPONSIVE_HEADER_CLASS_NAME}>
|
|
805
|
-
<div className="grid min-w-0 flex-1 grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
|
806
|
-
<SideSummary log={left} side="left" displayNumber={leftDisplayNumber} />
|
|
807
|
-
<SideSummary log={right} side="right" displayNumber={rightDisplayNumber} />
|
|
808
|
-
</div>
|
|
809
|
-
<div className="flex w-full shrink-0 items-center justify-between gap-2 sm:w-auto sm:justify-start">
|
|
810
|
-
<ModeToggle mode={mode} onChange={setMode} />
|
|
811
|
-
<button
|
|
812
|
-
ref={closeButtonRef}
|
|
813
|
-
type="button"
|
|
814
|
-
onClick={onClose}
|
|
815
|
-
aria-label="Close"
|
|
816
|
-
className="p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer"
|
|
817
|
-
>
|
|
818
|
-
<X className="size-4" />
|
|
819
|
-
</button>
|
|
820
|
-
</div>
|
|
821
|
-
</div>
|
|
822
|
-
|
|
823
|
-
{!sameSession && (
|
|
824
|
-
<div className="border-b border-border bg-warning/10 px-4 py-1.5 text-xs text-foreground">
|
|
825
|
-
Heads up: the two selected logs are from different sessions.
|
|
826
|
-
</div>
|
|
827
|
-
)}
|
|
828
|
-
|
|
829
|
-
{allEqual ? (
|
|
830
|
-
<div className="flex-1 min-h-0 overflow-y-auto flex items-center justify-center text-muted-foreground text-sm">
|
|
831
|
-
The two Request payloads are identical.
|
|
832
|
-
</div>
|
|
833
|
-
) : (
|
|
834
|
-
<>
|
|
835
|
-
<SummaryChips counts={counts} onJumpTo={jumpToKind} />
|
|
836
|
-
<div ref={bodyRef} className="flex-1 min-h-0 overflow-y-auto">
|
|
837
|
-
{mode === "unified" ? (
|
|
838
|
-
<div className="px-3 py-2 space-y-0.5">
|
|
839
|
-
{grouped.map((g, i) => {
|
|
840
|
-
if (g.kind === "equal-run") {
|
|
841
|
-
return (
|
|
842
|
-
<EqualRunRow
|
|
843
|
-
key={`r${i}`}
|
|
844
|
-
ops={g.ops}
|
|
845
|
-
expanded={expandedRuns.has(i)}
|
|
846
|
-
onToggle={() => toggleRun(i)}
|
|
847
|
-
/>
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
const op = g.op;
|
|
851
|
-
return (
|
|
852
|
-
<UnifiedOpRow
|
|
853
|
-
key={`o${i}`}
|
|
854
|
-
op={op}
|
|
855
|
-
idx={i}
|
|
856
|
-
copiedPath={copiedPath}
|
|
857
|
-
onCopyPath={onCopyPath}
|
|
858
|
-
expanded={expandedRows.has(i)}
|
|
859
|
-
onToggle={() => toggleRow(i)}
|
|
860
|
-
/>
|
|
861
|
-
);
|
|
862
|
-
})}
|
|
863
|
-
</div>
|
|
864
|
-
) : (
|
|
865
|
-
<SplitBody
|
|
866
|
-
grouped={grouped}
|
|
867
|
-
left={left}
|
|
868
|
-
right={right}
|
|
869
|
-
leftDisplayNumber={leftDisplayNumber}
|
|
870
|
-
rightDisplayNumber={rightDisplayNumber}
|
|
871
|
-
/>
|
|
872
|
-
)}
|
|
873
|
-
</div>
|
|
874
|
-
</>
|
|
875
|
-
)}
|
|
876
|
-
</div>
|
|
877
|
-
</div>
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/** Three-column on wide screens and a bounded stacked comparison on narrow screens. */
|
|
882
|
-
function SplitBody({
|
|
883
|
-
grouped,
|
|
884
|
-
left,
|
|
885
|
-
right,
|
|
886
|
-
leftDisplayNumber,
|
|
887
|
-
rightDisplayNumber,
|
|
888
|
-
}: {
|
|
889
|
-
grouped: GroupedOp[];
|
|
890
|
-
left: CapturedLog;
|
|
891
|
-
right: CapturedLog;
|
|
892
|
-
leftDisplayNumber: number;
|
|
893
|
-
rightDisplayNumber: number;
|
|
894
|
-
}): JSX.Element {
|
|
895
|
-
return (
|
|
896
|
-
<div data-compare-layout="responsive-split" className={COMPARE_RESPONSIVE_SPLIT_CLASS_NAME}>
|
|
897
|
-
{/* Column headers */}
|
|
898
|
-
<div className="col-span-1 mb-2 grid grid-cols-1 gap-x-2 gap-y-1 border-b border-border pb-2 text-[10px] uppercase tracking-wider text-muted-foreground md:col-span-3 md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]">
|
|
899
|
-
<span>Path</span>
|
|
900
|
-
<span title={`Log ID ${String(left.id)}`}>Left (Log #{leftDisplayNumber})</span>
|
|
901
|
-
<span title={`Log ID ${String(right.id)}`}>Right (Log #{rightDisplayNumber})</span>
|
|
902
|
-
</div>
|
|
903
|
-
|
|
904
|
-
{grouped.map((g, i) => {
|
|
905
|
-
if (g.kind === "equal-run") {
|
|
906
|
-
return (
|
|
907
|
-
<div
|
|
908
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
|
|
909
|
-
key={i}
|
|
910
|
-
className="col-span-1 px-2 py-1 text-xs text-muted-foreground/60 md:col-span-3"
|
|
911
|
-
>
|
|
912
|
-
{g.ops.length} equal siblings collapsed — switch to Unified to expand
|
|
913
|
-
</div>
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
const op = g.op;
|
|
917
|
-
if (op.kind === "equal") {
|
|
918
|
-
return (
|
|
919
|
-
<div
|
|
920
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
|
|
921
|
-
key={i}
|
|
922
|
-
className="col-span-1 grid min-w-0 grid-cols-1 gap-x-2 gap-y-1 px-2 py-0.5 text-muted-foreground md:col-span-3 md:grid-cols-[200px_minmax(0,1fr)_minmax(0,1fr)]"
|
|
923
|
-
>
|
|
924
|
-
<span className="font-mono text-xs truncate" title={op.path}>
|
|
925
|
-
{op.path}
|
|
926
|
-
</span>
|
|
927
|
-
<span className="font-mono text-xs break-all opacity-60">
|
|
928
|
-
{previewNode(op.value, 200)}
|
|
929
|
-
</span>
|
|
930
|
-
<span className="font-mono text-xs break-all opacity-60">
|
|
931
|
-
{previewNode(op.value, 200)}
|
|
932
|
-
</span>
|
|
933
|
-
</div>
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
if (op.kind === "added") {
|
|
937
|
-
return (
|
|
938
|
-
<div
|
|
939
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
|
|
940
|
-
key={i}
|
|
941
|
-
className="col-span-1 rounded border-l-2 border-l-status-success bg-status-success/5 px-2 py-1 text-xs md:col-span-3"
|
|
942
|
-
>
|
|
943
|
-
<div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
|
|
944
|
-
<div className="font-mono break-all text-status-success">
|
|
945
|
-
+ {previewNode(op.value, 400)}
|
|
946
|
-
</div>
|
|
947
|
-
</div>
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
if (op.kind === "removed") {
|
|
951
|
-
return (
|
|
952
|
-
<div
|
|
953
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
|
|
954
|
-
key={i}
|
|
955
|
-
className="col-span-1 rounded border-l-2 border-l-status-danger bg-status-danger/5 px-2 py-1 text-xs md:col-span-3"
|
|
956
|
-
>
|
|
957
|
-
<div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
|
|
958
|
-
<div className="font-mono break-all text-status-danger line-through">
|
|
959
|
-
− {previewNode(op.value, 400)}
|
|
960
|
-
</div>
|
|
961
|
-
</div>
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
return (
|
|
965
|
-
<div
|
|
966
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
|
|
967
|
-
key={i}
|
|
968
|
-
className="col-span-1 rounded border-l-2 border-l-status-watch bg-status-watch/5 px-2 py-1 text-xs md:col-span-3"
|
|
969
|
-
>
|
|
970
|
-
<div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
|
|
971
|
-
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
972
|
-
<div className="font-mono break-all text-status-danger line-through">
|
|
973
|
-
{previewNode(op.left, 400)}
|
|
974
|
-
</div>
|
|
975
|
-
<div className="font-mono break-all text-status-success">
|
|
976
|
-
{previewNode(op.right, 400)}
|
|
977
|
-
</div>
|
|
978
|
-
</div>
|
|
979
|
-
</div>
|
|
980
|
-
);
|
|
981
|
-
})}
|
|
982
|
-
</div>
|
|
983
|
-
);
|
|
984
|
-
}
|