@principal-ai/subsystems-react 0.17.0
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/README.md +93 -0
- package/dist/components/ConfigurationSelector.d.ts +35 -0
- package/dist/components/ConfigurationSelector.d.ts.map +1 -0
- package/dist/components/ConfigurationSelector.js +63 -0
- package/dist/components/ConfigurationSelector.js.map +1 -0
- package/dist/components/GraphRenderer.d.ts +295 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -0
- package/dist/components/GraphRenderer.js +2008 -0
- package/dist/components/GraphRenderer.js.map +1 -0
- package/dist/components/MultiCanvasRenderer.d.ts +75 -0
- package/dist/components/MultiCanvasRenderer.d.ts.map +1 -0
- package/dist/components/MultiCanvasRenderer.js +203 -0
- package/dist/components/MultiCanvasRenderer.js.map +1 -0
- package/dist/components/NodeTooltip.d.ts +24 -0
- package/dist/components/NodeTooltip.d.ts.map +1 -0
- package/dist/components/NodeTooltip.js +160 -0
- package/dist/components/NodeTooltip.js.map +1 -0
- package/dist/components/SelectionSidebar.d.ts +18 -0
- package/dist/components/SelectionSidebar.d.ts.map +1 -0
- package/dist/components/SelectionSidebar.js +190 -0
- package/dist/components/SelectionSidebar.js.map +1 -0
- package/dist/components/SequenceDiagramRenderer.d.ts +77 -0
- package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -0
- package/dist/components/SequenceDiagramRenderer.js +604 -0
- package/dist/components/SequenceDiagramRenderer.js.map +1 -0
- package/dist/components/WorkflowSequenceDiagram.d.ts +63 -0
- package/dist/components/WorkflowSequenceDiagram.d.ts.map +1 -0
- package/dist/components/WorkflowSequenceDiagram.js +147 -0
- package/dist/components/WorkflowSequenceDiagram.js.map +1 -0
- package/dist/components/dashboard/DashboardRenderer.d.ts +10 -0
- package/dist/components/dashboard/DashboardRenderer.d.ts.map +1 -0
- package/dist/components/dashboard/DashboardRenderer.js +179 -0
- package/dist/components/dashboard/DashboardRenderer.js.map +1 -0
- package/dist/components/dashboard/MetricPanel.d.ts +10 -0
- package/dist/components/dashboard/MetricPanel.d.ts.map +1 -0
- package/dist/components/dashboard/MetricPanel.js +103 -0
- package/dist/components/dashboard/MetricPanel.js.map +1 -0
- package/dist/components/dashboard/MockDataProvider.d.ts +30 -0
- package/dist/components/dashboard/MockDataProvider.d.ts.map +1 -0
- package/dist/components/dashboard/MockDataProvider.js +270 -0
- package/dist/components/dashboard/MockDataProvider.js.map +1 -0
- package/dist/components/dashboard/components/BarChart.d.ts +10 -0
- package/dist/components/dashboard/components/BarChart.d.ts.map +1 -0
- package/dist/components/dashboard/components/BarChart.js +167 -0
- package/dist/components/dashboard/components/BarChart.js.map +1 -0
- package/dist/components/dashboard/components/LineChart.d.ts +10 -0
- package/dist/components/dashboard/components/LineChart.d.ts.map +1 -0
- package/dist/components/dashboard/components/LineChart.js +141 -0
- package/dist/components/dashboard/components/LineChart.js.map +1 -0
- package/dist/components/dashboard/components/MetricCard.d.ts +9 -0
- package/dist/components/dashboard/components/MetricCard.d.ts.map +1 -0
- package/dist/components/dashboard/components/MetricCard.js +163 -0
- package/dist/components/dashboard/components/MetricCard.js.map +1 -0
- package/dist/components/dashboard/components/SourceLink.d.ts +9 -0
- package/dist/components/dashboard/components/SourceLink.d.ts.map +1 -0
- package/dist/components/dashboard/components/SourceLink.js +39 -0
- package/dist/components/dashboard/components/SourceLink.js.map +1 -0
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts +9 -0
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +1 -0
- package/dist/components/dashboard/components/TimeRangeSelector.js +167 -0
- package/dist/components/dashboard/components/TimeRangeSelector.js.map +1 -0
- package/dist/components/dashboard/components/index.d.ts +6 -0
- package/dist/components/dashboard/components/index.d.ts.map +1 -0
- package/dist/components/dashboard/components/index.js +6 -0
- package/dist/components/dashboard/components/index.js.map +1 -0
- package/dist/components/dashboard/index.d.ts +6 -0
- package/dist/components/dashboard/index.d.ts.map +1 -0
- package/dist/components/dashboard/index.js +8 -0
- package/dist/components/dashboard/index.js.map +1 -0
- package/dist/components/dashboard/types.d.ts +74 -0
- package/dist/components/dashboard/types.d.ts.map +1 -0
- package/dist/components/dashboard/types.js +8 -0
- package/dist/components/dashboard/types.js.map +1 -0
- package/dist/components/session-events/SessionEventFeed.d.ts +34 -0
- package/dist/components/session-events/SessionEventFeed.d.ts.map +1 -0
- package/dist/components/session-events/SessionEventFeed.js +1196 -0
- package/dist/components/session-events/SessionEventFeed.js.map +1 -0
- package/dist/components/session-events/index.d.ts +7 -0
- package/dist/components/session-events/index.d.ts.map +1 -0
- package/dist/components/session-events/index.js +6 -0
- package/dist/components/session-events/index.js.map +1 -0
- package/dist/components/state-view/PipelineView.d.ts +14 -0
- package/dist/components/state-view/PipelineView.d.ts.map +1 -0
- package/dist/components/state-view/PipelineView.js +195 -0
- package/dist/components/state-view/PipelineView.js.map +1 -0
- package/dist/components/state-view/index.d.ts +14 -0
- package/dist/components/state-view/index.d.ts.map +1 -0
- package/dist/components/state-view/index.js +12 -0
- package/dist/components/state-view/index.js.map +1 -0
- package/dist/components/state-view/types.d.ts +188 -0
- package/dist/components/state-view/types.d.ts.map +1 -0
- package/dist/components/state-view/types.js +10 -0
- package/dist/components/state-view/types.js.map +1 -0
- package/dist/components/state-view/useStateView.d.ts +32 -0
- package/dist/components/state-view/useStateView.d.ts.map +1 -0
- package/dist/components/state-view/useStateView.js +129 -0
- package/dist/components/state-view/useStateView.js.map +1 -0
- package/dist/contexts/GraphEditContext.d.ts +20 -0
- package/dist/contexts/GraphEditContext.d.ts.map +1 -0
- package/dist/contexts/GraphEditContext.js +8 -0
- package/dist/contexts/GraphEditContext.js.map +1 -0
- package/dist/contexts/TooltipPortalContext.d.ts +8 -0
- package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
- package/dist/contexts/TooltipPortalContext.js +8 -0
- package/dist/contexts/TooltipPortalContext.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +24 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -0
- package/dist/edges/CustomEdge.js +222 -0
- package/dist/edges/CustomEdge.js.map +1 -0
- package/dist/edges/GenericEdge.d.ts +18 -0
- package/dist/edges/GenericEdge.d.ts.map +1 -0
- package/dist/edges/GenericEdge.js +10 -0
- package/dist/edges/GenericEdge.js.map +1 -0
- package/dist/graphify/anchor.d.ts +28 -0
- package/dist/graphify/anchor.d.ts.map +1 -0
- package/dist/graphify/anchor.js +132 -0
- package/dist/graphify/anchor.js.map +1 -0
- package/dist/graphify/consolidated.d.ts +210 -0
- package/dist/graphify/consolidated.d.ts.map +1 -0
- package/dist/graphify/consolidated.js +11 -0
- package/dist/graphify/consolidated.js.map +1 -0
- package/dist/graphify/ids.d.ts +19 -0
- package/dist/graphify/ids.d.ts.map +1 -0
- package/dist/graphify/ids.js +46 -0
- package/dist/graphify/ids.js.map +1 -0
- package/dist/graphify/index.d.ts +18 -0
- package/dist/graphify/index.d.ts.map +1 -0
- package/dist/graphify/index.js +12 -0
- package/dist/graphify/index.js.map +1 -0
- package/dist/graphify/kind.d.ts +23 -0
- package/dist/graphify/kind.d.ts.map +1 -0
- package/dist/graphify/kind.js +77 -0
- package/dist/graphify/kind.js.map +1 -0
- package/dist/graphify/resolve.d.ts +48 -0
- package/dist/graphify/resolve.d.ts.map +1 -0
- package/dist/graphify/resolve.js +83 -0
- package/dist/graphify/resolve.js.map +1 -0
- package/dist/graphify/signature.d.ts +87 -0
- package/dist/graphify/signature.d.ts.map +1 -0
- package/dist/graphify/signature.js +276 -0
- package/dist/graphify/signature.js.map +1 -0
- package/dist/graphify/types.d.ts +141 -0
- package/dist/graphify/types.d.ts.map +1 -0
- package/dist/graphify/types.js +8 -0
- package/dist/graphify/types.js.map +1 -0
- package/dist/hooks/useElkLayout.d.ts +66 -0
- package/dist/hooks/useElkLayout.d.ts.map +1 -0
- package/dist/hooks/useElkLayout.js +136 -0
- package/dist/hooks/useElkLayout.js.map +1 -0
- package/dist/hooks/usePathBasedEvents.d.ts +42 -0
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
- package/dist/hooks/usePathBasedEvents.js +119 -0
- package/dist/hooks/usePathBasedEvents.js.map +1 -0
- package/dist/hooks/useSequenceLayout.d.ts +186 -0
- package/dist/hooks/useSequenceLayout.d.ts.map +1 -0
- package/dist/hooks/useSequenceLayout.js +327 -0
- package/dist/hooks/useSequenceLayout.js.map +1 -0
- package/dist/hooks/useUndoRedo.d.ts +80 -0
- package/dist/hooks/useUndoRedo.d.ts.map +1 -0
- package/dist/hooks/useUndoRedo.js +70 -0
- package/dist/hooks/useUndoRedo.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts +28 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -0
- package/dist/nodes/CustomNode.js +820 -0
- package/dist/nodes/CustomNode.js.map +1 -0
- package/dist/nodes/GenericNode.d.ts +20 -0
- package/dist/nodes/GenericNode.d.ts.map +1 -0
- package/dist/nodes/GenericNode.js +20 -0
- package/dist/nodes/GenericNode.js.map +1 -0
- package/dist/nodes/otel/OtelBoundaryNode.d.ts +55 -0
- package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelBoundaryNode.js +81 -0
- package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -0
- package/dist/nodes/otel/OtelEventNode.d.ts +59 -0
- package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelEventNode.js +82 -0
- package/dist/nodes/otel/OtelEventNode.js.map +1 -0
- package/dist/nodes/otel/OtelResourceNode.d.ts +53 -0
- package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelResourceNode.js +105 -0
- package/dist/nodes/otel/OtelResourceNode.js.map +1 -0
- package/dist/nodes/otel/OtelScopeNode.d.ts +53 -0
- package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelScopeNode.js +81 -0
- package/dist/nodes/otel/OtelScopeNode.js.map +1 -0
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts +61 -0
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelSpanConventionNode.js +131 -0
- package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -0
- package/dist/nodes/otel/index.d.ts +17 -0
- package/dist/nodes/otel/index.d.ts.map +1 -0
- package/dist/nodes/otel/index.js +13 -0
- package/dist/nodes/otel/index.js.map +1 -0
- package/dist/nodes/otel/shared/NodeBadges.d.ts +10 -0
- package/dist/nodes/otel/shared/NodeBadges.d.ts.map +1 -0
- package/dist/nodes/otel/shared/NodeBadges.js +178 -0
- package/dist/nodes/otel/shared/NodeBadges.js.map +1 -0
- package/dist/nodes/otel/shared/NodeContent.d.ts +10 -0
- package/dist/nodes/otel/shared/NodeContent.d.ts.map +1 -0
- package/dist/nodes/otel/shared/NodeContent.js +96 -0
- package/dist/nodes/otel/shared/NodeContent.js.map +1 -0
- package/dist/nodes/otel/shared/index.d.ts +8 -0
- package/dist/nodes/otel/shared/index.d.ts.map +1 -0
- package/dist/nodes/otel/shared/index.js +8 -0
- package/dist/nodes/otel/shared/index.js.map +1 -0
- package/dist/nodes/otel/shared/types.d.ts +129 -0
- package/dist/nodes/otel/shared/types.d.ts.map +1 -0
- package/dist/nodes/otel/shared/types.js +5 -0
- package/dist/nodes/otel/shared/types.js.map +1 -0
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts +42 -0
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +1 -0
- package/dist/nodes/otel/shared/useNodeBehavior.js +61 -0
- package/dist/nodes/otel/shared/useNodeBehavior.js.map +1 -0
- package/dist/pierre/PierreFileView.d.ts +17 -0
- package/dist/pierre/PierreFileView.d.ts.map +1 -0
- package/dist/pierre/PierreFileView.js +46 -0
- package/dist/pierre/PierreFileView.js.map +1 -0
- package/dist/pierre/PierreSnippetView.d.ts +28 -0
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -0
- package/dist/pierre/PierreSnippetView.js +105 -0
- package/dist/pierre/PierreSnippetView.js.map +1 -0
- package/dist/pierre/PierreThroughlineCodeView.d.ts +22 -0
- package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
- package/dist/pierre/PierreThroughlineCodeView.js +206 -0
- package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
- package/dist/pierre/constructColors.d.ts +24 -0
- package/dist/pierre/constructColors.d.ts.map +1 -0
- package/dist/pierre/constructColors.js +91 -0
- package/dist/pierre/constructColors.js.map +1 -0
- package/dist/pierre/index.d.ts +12 -0
- package/dist/pierre/index.d.ts.map +1 -0
- package/dist/pierre/index.js +7 -0
- package/dist/pierre/index.js.map +1 -0
- package/dist/pierre/pierreBackground.d.ts +15 -0
- package/dist/pierre/pierreBackground.d.ts.map +1 -0
- package/dist/pierre/pierreBackground.js +31 -0
- package/dist/pierre/pierreBackground.js.map +1 -0
- package/dist/pierre/pierreFileLang.d.ts +27 -0
- package/dist/pierre/pierreFileLang.d.ts.map +1 -0
- package/dist/pierre/pierreFileLang.js +52 -0
- package/dist/pierre/pierreFileLang.js.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.js +20 -0
- package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
- package/dist/pierre/scrollAnchor.d.ts +5 -0
- package/dist/pierre/scrollAnchor.d.ts.map +1 -0
- package/dist/pierre/scrollAnchor.js +9 -0
- package/dist/pierre/scrollAnchor.js.map +1 -0
- package/dist/pierre/sliceSnippet.d.ts +10 -0
- package/dist/pierre/sliceSnippet.d.ts.map +1 -0
- package/dist/pierre/sliceSnippet.js +16 -0
- package/dist/pierre/sliceSnippet.js.map +1 -0
- package/dist/pierre/syntaxTokens.d.ts +11 -0
- package/dist/pierre/syntaxTokens.d.ts.map +1 -0
- package/dist/pierre/syntaxTokens.js +51 -0
- package/dist/pierre/syntaxTokens.js.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts +87 -0
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -0
- package/dist/subsystem/ComponentDeclaration.js +374 -0
- package/dist/subsystem/ComponentDeclaration.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +17 -0
- package/dist/subsystem/FileDrawer.d.ts.map +1 -0
- package/dist/subsystem/FileDrawer.js +72 -0
- package/dist/subsystem/FileDrawer.js.map +1 -0
- package/dist/subsystem/GraphLayoutCover.d.ts +20 -0
- package/dist/subsystem/GraphLayoutCover.d.ts.map +1 -0
- package/dist/subsystem/GraphLayoutCover.js +42 -0
- package/dist/subsystem/GraphLayoutCover.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +96 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.js +1059 -0
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -0
- package/dist/subsystem/SubsystemFileTree.d.ts +24 -0
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -0
- package/dist/subsystem/SubsystemFileTree.js +150 -0
- package/dist/subsystem/SubsystemFileTree.js.map +1 -0
- package/dist/subsystem/declarationRef.d.ts +39 -0
- package/dist/subsystem/declarationRef.d.ts.map +1 -0
- package/dist/subsystem/declarationRef.js +32 -0
- package/dist/subsystem/declarationRef.js.map +1 -0
- package/dist/subsystem/formatDeclaration.d.ts +11 -0
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -0
- package/dist/subsystem/formatDeclaration.js +146 -0
- package/dist/subsystem/formatDeclaration.js.map +1 -0
- package/dist/subsystem/model.d.ts +353 -0
- package/dist/subsystem/model.d.ts.map +1 -0
- package/dist/subsystem/model.js +503 -0
- package/dist/subsystem/model.js.map +1 -0
- package/dist/subsystem/nodes.d.ts +69 -0
- package/dist/subsystem/nodes.d.ts.map +1 -0
- package/dist/subsystem/nodes.js +289 -0
- package/dist/subsystem/nodes.js.map +1 -0
- package/dist/subsystem/paths.d.ts +63 -0
- package/dist/subsystem/paths.d.ts.map +1 -0
- package/dist/subsystem/paths.js +86 -0
- package/dist/subsystem/paths.js.map +1 -0
- package/dist/subsystem/tokenizeComponent.d.ts +20 -0
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -0
- package/dist/subsystem/tokenizeComponent.js +56 -0
- package/dist/subsystem/tokenizeComponent.js.map +1 -0
- package/dist/subsystem/tokenizeFormatted.d.ts +12 -0
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -0
- package/dist/subsystem/tokenizeFormatted.js +13 -0
- package/dist/subsystem/tokenizeFormatted.js.map +1 -0
- package/dist/utils/animationMapping.d.ts +53 -0
- package/dist/utils/animationMapping.d.ts.map +1 -0
- package/dist/utils/animationMapping.js +126 -0
- package/dist/utils/animationMapping.js.map +1 -0
- package/dist/utils/canvasBounds.d.ts +50 -0
- package/dist/utils/canvasBounds.d.ts.map +1 -0
- package/dist/utils/canvasBounds.js +88 -0
- package/dist/utils/canvasBounds.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +137 -0
- package/dist/utils/elkLayout.d.ts.map +1 -0
- package/dist/utils/elkLayout.js +596 -0
- package/dist/utils/elkLayout.js.map +1 -0
- package/dist/utils/graphConverter.d.ts +18 -0
- package/dist/utils/graphConverter.d.ts.map +1 -0
- package/dist/utils/graphConverter.js +114 -0
- package/dist/utils/graphConverter.js.map +1 -0
- package/dist/utils/iconResolver.d.ts +29 -0
- package/dist/utils/iconResolver.d.ts.map +1 -0
- package/dist/utils/iconResolver.js +30 -0
- package/dist/utils/iconResolver.js.map +1 -0
- package/dist/utils/orientationUtils.d.ts +19 -0
- package/dist/utils/orientationUtils.d.ts.map +1 -0
- package/dist/utils/orientationUtils.js +62 -0
- package/dist/utils/orientationUtils.js.map +1 -0
- package/package.json +80 -0
- package/src/components/ConfigurationSelector.tsx +133 -0
- package/src/components/GraphRenderer.tsx +3195 -0
- package/src/components/MultiCanvasRenderer.tsx +295 -0
- package/src/components/NodeTooltip.tsx +279 -0
- package/src/components/PendingChanges.test.tsx +375 -0
- package/src/components/SelectionSidebar.tsx +352 -0
- package/src/components/SequenceDiagramRenderer.tsx +1156 -0
- package/src/components/WorkflowSequenceDiagram.tsx +270 -0
- package/src/components/dashboard/DashboardRenderer.tsx +317 -0
- package/src/components/dashboard/MetricPanel.tsx +254 -0
- package/src/components/dashboard/MockDataProvider.ts +330 -0
- package/src/components/dashboard/components/BarChart.tsx +299 -0
- package/src/components/dashboard/components/LineChart.tsx +279 -0
- package/src/components/dashboard/components/MetricCard.tsx +270 -0
- package/src/components/dashboard/components/SourceLink.tsx +63 -0
- package/src/components/dashboard/components/TimeRangeSelector.tsx +280 -0
- package/src/components/dashboard/components/index.ts +5 -0
- package/src/components/dashboard/index.ts +47 -0
- package/src/components/dashboard/types.ts +126 -0
- package/src/components/session-events/SessionEventFeed.tsx +1797 -0
- package/src/components/session-events/index.ts +13 -0
- package/src/components/state-view/PipelineView.tsx +347 -0
- package/src/components/state-view/index.ts +14 -0
- package/src/components/state-view/types.ts +261 -0
- package/src/components/state-view/useStateView.ts +205 -0
- package/src/contexts/GraphEditContext.tsx +27 -0
- package/src/contexts/TooltipPortalContext.ts +8 -0
- package/src/edges/CustomEdge.tsx +402 -0
- package/src/edges/GenericEdge.tsx +36 -0
- package/src/graphify/anchor.test.ts +124 -0
- package/src/graphify/anchor.ts +160 -0
- package/src/graphify/consolidated.ts +246 -0
- package/src/graphify/ids.ts +48 -0
- package/src/graphify/index.ts +72 -0
- package/src/graphify/kind.test.ts +108 -0
- package/src/graphify/kind.ts +120 -0
- package/src/graphify/resolve.test.ts +89 -0
- package/src/graphify/resolve.ts +115 -0
- package/src/graphify/signature.test.ts +278 -0
- package/src/graphify/signature.ts +354 -0
- package/src/graphify/types.ts +164 -0
- package/src/hooks/useElkLayout.test.ts +134 -0
- package/src/hooks/useElkLayout.ts +191 -0
- package/src/hooks/usePathBasedEvents.ts +192 -0
- package/src/hooks/useSequenceLayout.ts +543 -0
- package/src/hooks/useUndoRedo.ts +144 -0
- package/src/index.ts +341 -0
- package/src/nodes/CustomNode.tsx +1231 -0
- package/src/nodes/GenericNode.tsx +55 -0
- package/src/nodes/otel/OtelBoundaryNode.tsx +224 -0
- package/src/nodes/otel/OtelEventNode.tsx +225 -0
- package/src/nodes/otel/OtelResourceNode.tsx +251 -0
- package/src/nodes/otel/OtelScopeNode.tsx +221 -0
- package/src/nodes/otel/OtelSpanConventionNode.tsx +295 -0
- package/src/nodes/otel/index.ts +23 -0
- package/src/nodes/otel/shared/NodeBadges.tsx +295 -0
- package/src/nodes/otel/shared/NodeContent.tsx +204 -0
- package/src/nodes/otel/shared/index.ts +8 -0
- package/src/nodes/otel/shared/types.ts +138 -0
- package/src/nodes/otel/shared/useNodeBehavior.ts +114 -0
- package/src/pierre/PierreFileView.tsx +81 -0
- package/src/pierre/PierreSnippetView.tsx +184 -0
- package/src/pierre/PierreThroughlineCodeView.tsx +304 -0
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -0
- package/src/pierre/index.ts +18 -0
- package/src/pierre/pierreBackground.ts +33 -0
- package/src/pierre/pierreFileLang.test.ts +26 -0
- package/src/pierre/pierreFileLang.ts +55 -0
- package/src/pierre/pierreSyntaxTheme.ts +23 -0
- package/src/pierre/scrollAnchor.test.ts +13 -0
- package/src/pierre/scrollAnchor.ts +11 -0
- package/src/pierre/sliceSnippet.test.ts +21 -0
- package/src/pierre/sliceSnippet.ts +31 -0
- package/src/pierre/syntaxTokens.test.ts +35 -0
- package/src/pierre/syntaxTokens.ts +60 -0
- package/src/stories/Audit/BoundaryNodes.stories.tsx +385 -0
- package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +1448 -0
- package/src/stories/Audit/CanvasNodeTypes.stories.tsx +1049 -0
- package/src/stories/Audit/ColorPriority.stories.tsx +895 -0
- package/src/stories/Audit/DiamondBadges.stories.tsx +629 -0
- package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +724 -0
- package/src/stories/Audit/NodeFieldsAudit.stories.tsx +996 -0
- package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +715 -0
- package/src/stories/Audit/NodeShapes.stories.tsx +1208 -0
- package/src/stories/Audit/WideNodesTesting.stories.tsx +1045 -0
- package/src/stories/Components/DashboardRenderer.stories.tsx +263 -0
- package/src/stories/Components/GraphRenderer.stories.tsx +3236 -0
- package/src/stories/Components/MultiCanvasRenderer.stories.tsx +370 -0
- package/src/stories/Components/SequenceDiagram.stories.tsx +255 -0
- package/src/stories/Components/StateView.stories.tsx +417 -0
- package/src/stories/Components/TraceViewer.stories.tsx +442 -0
- package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +802 -0
- package/src/stories/Features/AnimationWorkshop.stories.tsx +727 -0
- package/src/stories/Features/ChangeTypeVisual.stories.tsx +67 -0
- package/src/stories/Features/EventDrivenAnimations.stories.tsx +513 -0
- package/src/stories/Features/MultiConfig.stories.tsx +539 -0
- package/src/stories/Introduction.mdx +34 -0
- package/src/stories/Layout/ElkEdgeRouting.stories.tsx +539 -0
- package/src/stories/Layout/GraphOrientation.stories.tsx +500 -0
- package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +352 -0
- package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +560 -0
- package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +491 -0
- package/src/stories/Nodes/NumericColors.stories.tsx +440 -0
- package/src/stories/OTEL/EventNameDisplay.stories.tsx +694 -0
- package/src/stories/OTEL/EventNameFallback.stories.tsx +578 -0
- package/src/stories/OTEL/OtelComponents.stories.tsx +422 -0
- package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +806 -0
- package/src/stories/OTEL/RawEventCards.stories.tsx +57 -0
- package/src/stories/OTEL/SpanBadges.stories.tsx +840 -0
- package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +209 -0
- package/src/stories/Pierre/CodeView.stories.tsx +219 -0
- package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +721 -0
- package/src/stories/RealWorld/FileCitySequence.stories.tsx +516 -0
- package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +336 -0
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +321 -0
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +239 -0
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +216 -0
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +70 -0
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +68 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +681 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
- package/src/stories/__traces__/test-run.canvas.json +241 -0
- package/src/stories/data/activity-feed-analytics.dashboard.json +300 -0
- package/src/stories/data/file-city-images-transformed.otel.canvas.json +295 -0
- package/src/stories/data/file-city-workflows.json +269 -0
- package/src/stories/data/graph-converter-test-execution.json +443 -0
- package/src/stories/data/graph-converter-test.workflow.json +105 -0
- package/src/stories/data/graph-converter-validated-execution.json +58 -0
- package/src/stories/data/opencode-session-raw-events.json +273396 -0
- package/src/subsystem/ComponentDeclaration.tsx +643 -0
- package/src/subsystem/FileDrawer.tsx +106 -0
- package/src/subsystem/GraphLayoutCover.tsx +69 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +1644 -0
- package/src/subsystem/SubsystemFileTree.tsx +190 -0
- package/src/subsystem/declarationRef.test.ts +33 -0
- package/src/subsystem/declarationRef.ts +63 -0
- package/src/subsystem/formatDeclaration.ts +174 -0
- package/src/subsystem/model.test.ts +244 -0
- package/src/subsystem/model.ts +850 -0
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +443 -0
- package/src/subsystem/paths.test.ts +123 -0
- package/src/subsystem/paths.ts +127 -0
- package/src/subsystem/tokenizeComponent.ts +70 -0
- package/src/subsystem/tokenizeFormatted.ts +19 -0
- package/src/utils/animationMapping.ts +166 -0
- package/src/utils/canvasBounds.ts +146 -0
- package/src/utils/elkLayout.test.ts +240 -0
- package/src/utils/elkLayout.ts +779 -0
- package/src/utils/graphConverter.ts +148 -0
- package/src/utils/iconResolver.tsx +53 -0
- package/src/utils/orientationUtils.ts +73 -0
|
@@ -0,0 +1,1797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionEventFeed — a three-column diagnostic feed over one agent session's
|
|
3
|
+
* events, showing the raw → repo-normalized → accumulated (UI) pipeline
|
|
4
|
+
* for every row.
|
|
5
|
+
*
|
|
6
|
+
* Input rows match the principal-studio's `SessionEventRow` wire shape:
|
|
7
|
+
* { seq, type, raw, normalized, accumulated }
|
|
8
|
+
* `raw` is the agent-specific payload (opencode V1 sqlite rows, or durable
|
|
9
|
+
* reader blobs such as Cursor store.db messages), `normalized` its
|
|
10
|
+
* repo-normalized form, and `accumulated` the AgentSessionEvent the File City
|
|
11
|
+
* UI actually renders (null when the accumulator drops the event).
|
|
12
|
+
*
|
|
13
|
+
* Dark diagnostic palette — intentionally self-contained rather than themed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
17
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
|
+
import { Check, Copy } from 'lucide-react';
|
|
19
|
+
import type {
|
|
20
|
+
AgentSessionEvent,
|
|
21
|
+
NormalizedPathInfo,
|
|
22
|
+
RepoNormalizedUniversalAgentSessionEvent,
|
|
23
|
+
V1RawEvent,
|
|
24
|
+
} from '@principal-ai/agent-monitoring';
|
|
25
|
+
|
|
26
|
+
export interface SessionEventFeedRow {
|
|
27
|
+
seq: number;
|
|
28
|
+
type: string;
|
|
29
|
+
raw: unknown;
|
|
30
|
+
normalized?: Record<string, unknown>;
|
|
31
|
+
accumulated: AgentSessionEvent | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SessionEventFeedProps {
|
|
35
|
+
title?: string;
|
|
36
|
+
rows: SessionEventFeedRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SessionEventFeedGroupedProps {
|
|
40
|
+
title?: string;
|
|
41
|
+
rows: SessionEventFeedRow[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type AnyRecord = Record<string, unknown>;
|
|
45
|
+
type NormalizedFeedEvent = Omit<RepoNormalizedUniversalAgentSessionEvent, 'raw'>;
|
|
46
|
+
|
|
47
|
+
/** Shared collapse state keyed by row seq. */
|
|
48
|
+
function useCollapse(seqs: number[], defaultCollapsed = false) {
|
|
49
|
+
const [collapsed, setCollapsed] = useState<Set<number>>(
|
|
50
|
+
() => (defaultCollapsed ? new Set(seqs) : new Set<number>()),
|
|
51
|
+
);
|
|
52
|
+
// When the feed grows (incremental paging appends rows), default newly added
|
|
53
|
+
// rows to collapsed so the invariant holds for rows that arrive after mount.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!defaultCollapsed) return;
|
|
56
|
+
setCollapsed((prev) => {
|
|
57
|
+
let changed = false;
|
|
58
|
+
const next = new Set(prev);
|
|
59
|
+
for (const seq of seqs) {
|
|
60
|
+
if (!next.has(seq)) {
|
|
61
|
+
next.add(seq);
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return changed ? next : prev;
|
|
66
|
+
});
|
|
67
|
+
}, [seqs, defaultCollapsed]);
|
|
68
|
+
const toggle = useCallback((seq: number) => {
|
|
69
|
+
setCollapsed((prev) => {
|
|
70
|
+
const next = new Set(prev);
|
|
71
|
+
if (next.has(seq)) next.delete(seq);
|
|
72
|
+
else next.add(seq);
|
|
73
|
+
return next;
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
const setAll = useCallback(
|
|
77
|
+
(value: boolean) => setCollapsed(value ? new Set(seqs) : new Set<number>()),
|
|
78
|
+
[seqs],
|
|
79
|
+
);
|
|
80
|
+
return { collapsed, toggle, setAll };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toRaw(raw: unknown): V1RawEvent | null {
|
|
84
|
+
if (raw === null || raw === undefined || typeof raw !== 'object') return null;
|
|
85
|
+
return raw as V1RawEvent;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toNormalized(normalized?: Record<string, unknown>): NormalizedFeedEvent | undefined {
|
|
89
|
+
return normalized as NormalizedFeedEvent | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Raw event → presentable pieces
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
function dataOf(e: V1RawEvent | null): AnyRecord {
|
|
97
|
+
return (e?.data ?? {}) as AnyRecord;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function partOf(e: V1RawEvent | null): AnyRecord | undefined {
|
|
101
|
+
return dataOf(e).part as AnyRecord | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when the payload looks like an opencode V1 sqlite event row. */
|
|
105
|
+
function isV1RawEvent(e: V1RawEvent | null): boolean {
|
|
106
|
+
return typeof e?.type === 'string' && e.type.length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cursor store.db blobs (and other durable readers) put the whole message in
|
|
111
|
+
* `raw` — no `type` / `data` envelope. Pull a presentable kind + summary from
|
|
112
|
+
* the common shapes CursorSessionReader emits:
|
|
113
|
+
* - chat message: `{ role, content, id? }`
|
|
114
|
+
* - tool wrapper: `{ message, block }` where block is tool-call / tool-result
|
|
115
|
+
* - session meta: `{ title?, cwd?, schemaVersion? }`
|
|
116
|
+
*/
|
|
117
|
+
function durableRawKind(e: AnyRecord): string {
|
|
118
|
+
const block = e.block as AnyRecord | undefined;
|
|
119
|
+
if (block && typeof block === 'object') {
|
|
120
|
+
const toolName = (block.toolName ?? block.name) as string | undefined;
|
|
121
|
+
const blockType = block.type as string | undefined;
|
|
122
|
+
if (blockType === 'tool-call' || blockType === 'tool_use') {
|
|
123
|
+
return toolName ? `tool-call:${toolName}` : 'tool-call';
|
|
124
|
+
}
|
|
125
|
+
if (blockType === 'tool-result' || blockType === 'tool_result') {
|
|
126
|
+
return toolName ? `tool-result:${toolName}` : 'tool-result';
|
|
127
|
+
}
|
|
128
|
+
if (typeof blockType === 'string') return blockType;
|
|
129
|
+
}
|
|
130
|
+
if (typeof e.role === 'string') return e.role;
|
|
131
|
+
if (typeof e.title === 'string' || typeof e.cwd === 'string' || typeof e.schemaVersion === 'number') {
|
|
132
|
+
return 'session-meta';
|
|
133
|
+
}
|
|
134
|
+
return 'durable-raw';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function durableRawDescribe(e: AnyRecord): string {
|
|
138
|
+
const block = e.block as AnyRecord | undefined;
|
|
139
|
+
if (block && typeof block === 'object') {
|
|
140
|
+
const toolName = (block.toolName ?? block.name) as string | undefined;
|
|
141
|
+
const blockType = block.type as string | undefined;
|
|
142
|
+
if (blockType === 'tool-call' || blockType === 'tool_use') {
|
|
143
|
+
const args = block.args ?? block.input;
|
|
144
|
+
const summary = toolSummary(args);
|
|
145
|
+
return [toolName, summary].filter(Boolean).join(' · ') || 'tool call';
|
|
146
|
+
}
|
|
147
|
+
if (blockType === 'tool-result' || blockType === 'tool_result') {
|
|
148
|
+
const result = block.result;
|
|
149
|
+
const text =
|
|
150
|
+
typeof result === 'string'
|
|
151
|
+
? result.slice(0, 120)
|
|
152
|
+
: result !== undefined
|
|
153
|
+
? JSON.stringify(result).slice(0, 120)
|
|
154
|
+
: '';
|
|
155
|
+
return [toolName, text].filter(Boolean).join(' · ') || 'tool result';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (typeof e.role === 'string') {
|
|
159
|
+
const content = e.content;
|
|
160
|
+
if (typeof content === 'string') {
|
|
161
|
+
const tagged = content.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
|
|
162
|
+
const text = (tagged?.[1] ?? content).trim().replace(/\s+/g, ' ');
|
|
163
|
+
return text.slice(0, 120) || e.role;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(content)) {
|
|
166
|
+
const texts = content
|
|
167
|
+
.filter((c): c is AnyRecord => !!c && typeof c === 'object')
|
|
168
|
+
.map((c) => (typeof c.text === 'string' ? c.text : c.toolName ?? c.name ?? c.type))
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
return texts.join(' · ').slice(0, 120) || e.role;
|
|
171
|
+
}
|
|
172
|
+
return e.role;
|
|
173
|
+
}
|
|
174
|
+
if (typeof e.title === 'string') return e.title;
|
|
175
|
+
if (typeof e.cwd === 'string') return e.cwd;
|
|
176
|
+
return 'durable raw event';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function eventTimestamp(e: V1RawEvent | null): number {
|
|
180
|
+
if (!e) return 0;
|
|
181
|
+
const d = dataOf(e);
|
|
182
|
+
if (typeof d.time === 'number') return d.time;
|
|
183
|
+
const info = d.info as AnyRecord | undefined;
|
|
184
|
+
const infoTime = info?.time as AnyRecord | undefined;
|
|
185
|
+
if (typeof infoTime?.created === 'number') return infoTime.created;
|
|
186
|
+
const part = partOf(e);
|
|
187
|
+
if (typeof part?.time === 'number') return part.time;
|
|
188
|
+
const state = part?.state as AnyRecord | undefined;
|
|
189
|
+
const stateTime = state?.time as AnyRecord | undefined;
|
|
190
|
+
if (typeof stateTime?.start === 'number') return stateTime.start;
|
|
191
|
+
if (typeof stateTime?.end === 'number') return stateTime.end;
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Event categorization (drives the preset filters)
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
type EventCategory = 'session' | 'conversation' | 'tool' | 'step' | 'patch' | 'compaction';
|
|
200
|
+
|
|
201
|
+
const ALL_CATEGORIES: EventCategory[] = [
|
|
202
|
+
'session',
|
|
203
|
+
'conversation',
|
|
204
|
+
'tool',
|
|
205
|
+
'step',
|
|
206
|
+
'patch',
|
|
207
|
+
'compaction',
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
function eventCategory(e: V1RawEvent | null): EventCategory {
|
|
211
|
+
if (!e) return 'session';
|
|
212
|
+
if (!isV1RawEvent(e)) {
|
|
213
|
+
const kind = durableRawKind(e as unknown as AnyRecord);
|
|
214
|
+
if (kind.startsWith('tool-call') || kind.startsWith('tool-result') || kind.startsWith('tool')) {
|
|
215
|
+
return 'tool';
|
|
216
|
+
}
|
|
217
|
+
if (kind === 'session-meta') return 'session';
|
|
218
|
+
return 'conversation';
|
|
219
|
+
}
|
|
220
|
+
switch (e.type) {
|
|
221
|
+
case 'session.created.1':
|
|
222
|
+
case 'session.updated.1':
|
|
223
|
+
return 'session';
|
|
224
|
+
case 'message.updated.1':
|
|
225
|
+
case 'message.removed.1':
|
|
226
|
+
case 'message.part.updated.1': {
|
|
227
|
+
const partType = partOf(e)?.type;
|
|
228
|
+
switch (partType) {
|
|
229
|
+
case 'text':
|
|
230
|
+
case 'reasoning':
|
|
231
|
+
return 'conversation';
|
|
232
|
+
case 'tool':
|
|
233
|
+
return 'tool';
|
|
234
|
+
case 'step-start':
|
|
235
|
+
case 'step-finish':
|
|
236
|
+
return 'step';
|
|
237
|
+
case 'patch':
|
|
238
|
+
return 'patch';
|
|
239
|
+
case 'compaction':
|
|
240
|
+
return 'compaction';
|
|
241
|
+
default:
|
|
242
|
+
return 'conversation';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
return 'conversation';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const PRESETS: Array<{ id: string; label: string; include: EventCategory[] }> = [
|
|
251
|
+
{ id: 'all', label: 'All', include: ALL_CATEGORIES },
|
|
252
|
+
{ id: 'activity', label: 'Activity', include: ['conversation', 'tool', 'patch'] },
|
|
253
|
+
{ id: 'conversation', label: 'Conversation', include: ['conversation'] },
|
|
254
|
+
{ id: 'tools', label: 'Tools', include: ['tool'] },
|
|
255
|
+
{ id: 'noise', label: 'Session & steps', include: ['session', 'step', 'compaction'] },
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
/** Granular key for a card (raw event type, or part type for parts). */
|
|
259
|
+
function eventKind(e: V1RawEvent | null): string {
|
|
260
|
+
if (!e) return 'no-raw';
|
|
261
|
+
if (!isV1RawEvent(e)) return durableRawKind(e as unknown as AnyRecord);
|
|
262
|
+
if (e.type === 'message.part.updated.1') {
|
|
263
|
+
return `part:${partOf(e)?.type ?? 'unknown'}`;
|
|
264
|
+
}
|
|
265
|
+
return e.type;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function kindLabel(kind: string | undefined | null): string {
|
|
269
|
+
if (!kind) return 'unknown';
|
|
270
|
+
if (kind.startsWith('part:')) return `part · ${kind.slice(5)}`;
|
|
271
|
+
if (kind.startsWith('tool-call:')) return `call · ${kind.slice('tool-call:'.length)}`;
|
|
272
|
+
if (kind.startsWith('tool-result:')) return `result · ${kind.slice('tool-result:'.length)}`;
|
|
273
|
+
return kind;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const KIND_COLORS: Record<string, string> = {
|
|
277
|
+
'session.created.1': '#64748b',
|
|
278
|
+
'session.updated.1': '#94a3b8',
|
|
279
|
+
'message.updated.1': '#06b6d4',
|
|
280
|
+
'message.removed.1': '#ef4444',
|
|
281
|
+
'part:text': '#10b981',
|
|
282
|
+
'part:reasoning': '#8b5cf6',
|
|
283
|
+
'part:tool': '#3b82f6',
|
|
284
|
+
'part:step-start': '#f59e0b',
|
|
285
|
+
'part:step-finish': '#f59e0b',
|
|
286
|
+
'part:patch': '#ef4444',
|
|
287
|
+
'part:compaction': '#ec4899',
|
|
288
|
+
'part:unknown': '#6b7280',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function kindColor(kind: string): string {
|
|
292
|
+
return KIND_COLORS[kind] ?? '#6b7280';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function describe(e: V1RawEvent | null): string {
|
|
296
|
+
if (!e) return 'finished';
|
|
297
|
+
if (!isV1RawEvent(e)) return durableRawDescribe(e as unknown as AnyRecord);
|
|
298
|
+
const d = dataOf(e);
|
|
299
|
+
const info = d.info as AnyRecord | undefined;
|
|
300
|
+
const part = partOf(e);
|
|
301
|
+
const state = part?.state as AnyRecord | undefined;
|
|
302
|
+
|
|
303
|
+
switch (e.type) {
|
|
304
|
+
case 'session.created.1':
|
|
305
|
+
return (info?.title as string) ?? 'Session created';
|
|
306
|
+
case 'session.updated.1': {
|
|
307
|
+
const bits: string[] = [];
|
|
308
|
+
const tokens = info?.tokens as AnyRecord | undefined;
|
|
309
|
+
if (typeof info?.cost === 'number') bits.push(`$${info.cost.toFixed(4)}`);
|
|
310
|
+
if (typeof tokens?.input === 'number') bits.push(`${tokens.input} in`);
|
|
311
|
+
if (typeof tokens?.output === 'number') bits.push(`${tokens.output} out`);
|
|
312
|
+
return bits.length > 0 ? `Session updated · ${bits.join(' · ')}` : 'Session updated';
|
|
313
|
+
}
|
|
314
|
+
case 'message.updated.1': {
|
|
315
|
+
const role = (info?.role as string) ?? 'message';
|
|
316
|
+
const model = (info?.model as AnyRecord | undefined)?.modelID as string | undefined;
|
|
317
|
+
return model ? `${role} message · ${model}` : `${role} message`;
|
|
318
|
+
}
|
|
319
|
+
case 'message.removed.1':
|
|
320
|
+
return `Removed ${d.messageID as string}`;
|
|
321
|
+
case 'message.part.updated.1': {
|
|
322
|
+
const partType = part?.type as string;
|
|
323
|
+
const toolName = part?.tool as string | undefined;
|
|
324
|
+
const status = state?.status as string | undefined;
|
|
325
|
+
switch (partType) {
|
|
326
|
+
case 'text':
|
|
327
|
+
return (part?.text as string) ?? 'Text';
|
|
328
|
+
case 'reasoning':
|
|
329
|
+
return (part?.text as string) || 'Reasoning';
|
|
330
|
+
case 'tool': {
|
|
331
|
+
const bits = [toolName, status];
|
|
332
|
+
const n = todoCountOf(state?.input);
|
|
333
|
+
if (n > 0) bits.push(`${n} todo${n === 1 ? '' : 's'}`);
|
|
334
|
+
return bits.filter(Boolean).join(' · ') || 'Tool';
|
|
335
|
+
}
|
|
336
|
+
case 'step-start':
|
|
337
|
+
return 'Step start';
|
|
338
|
+
case 'step-finish':
|
|
339
|
+
return part?.reason ? `Step finish · ${part.reason as string}` : 'Step finish';
|
|
340
|
+
case 'patch': {
|
|
341
|
+
const files = part?.files as string[] | undefined;
|
|
342
|
+
return `Patch · ${files?.length ?? 0} file${(files?.length ?? 0) === 1 ? '' : 's'}`;
|
|
343
|
+
}
|
|
344
|
+
case 'compaction':
|
|
345
|
+
return `Compaction${part?.auto ? ' · auto' : ''}`;
|
|
346
|
+
default:
|
|
347
|
+
return partType ?? 'Part';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
default:
|
|
351
|
+
return e.type;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Number of todos carried by a todowrite tool input, if any. */
|
|
356
|
+
function todoCountOf(input: unknown): number {
|
|
357
|
+
if (input && typeof input === 'object') {
|
|
358
|
+
const todos = (input as AnyRecord)['todos'];
|
|
359
|
+
if (Array.isArray(todos)) return todos.length;
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function toolSummary(input: unknown): string {
|
|
365
|
+
if (input === null || input === undefined) return '';
|
|
366
|
+
if (typeof input === 'string') return input;
|
|
367
|
+
if (Array.isArray(input)) return JSON.stringify(input).slice(0, 120);
|
|
368
|
+
const obj = input as AnyRecord;
|
|
369
|
+
// A todowrite input carries the full todo list — summarize by count rather
|
|
370
|
+
// than dumping truncated JSON.
|
|
371
|
+
if (Array.isArray(obj['todos'])) return `${(obj['todos'] as unknown[]).length} todos`;
|
|
372
|
+
for (const key of ['command', 'filePath', 'file_path', 'path', 'pattern', 'include', 'tool']) {
|
|
373
|
+
if (typeof obj[key] === 'string' && obj[key] !== '') return obj[key] as string;
|
|
374
|
+
}
|
|
375
|
+
return JSON.stringify(input).slice(0, 120);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// =============================================================================
|
|
379
|
+
// Cards
|
|
380
|
+
// =============================================================================
|
|
381
|
+
|
|
382
|
+
function formatTime(ts: number): string {
|
|
383
|
+
if (!ts) return '';
|
|
384
|
+
return new Date(ts).toLocaleTimeString([], {
|
|
385
|
+
hour: '2-digit',
|
|
386
|
+
minute: '2-digit',
|
|
387
|
+
second: '2-digit',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function RawEventCard({
|
|
392
|
+
event,
|
|
393
|
+
runCount,
|
|
394
|
+
snapshotCount,
|
|
395
|
+
expanded,
|
|
396
|
+
onToggle,
|
|
397
|
+
}: {
|
|
398
|
+
event: V1RawEvent | null;
|
|
399
|
+
runCount?: number;
|
|
400
|
+
snapshotCount?: number;
|
|
401
|
+
expanded: boolean;
|
|
402
|
+
onToggle: () => void;
|
|
403
|
+
}) {
|
|
404
|
+
const { theme } = useTheme();
|
|
405
|
+
const kind = eventKind(event);
|
|
406
|
+
const d = dataOf(event);
|
|
407
|
+
const part = partOf(event);
|
|
408
|
+
const state = part?.state as AnyRecord | undefined;
|
|
409
|
+
const color = kindColor(kind);
|
|
410
|
+
|
|
411
|
+
if (!event) {
|
|
412
|
+
return (
|
|
413
|
+
<div
|
|
414
|
+
style={{
|
|
415
|
+
display: 'flex',
|
|
416
|
+
alignItems: 'center',
|
|
417
|
+
gap: 8,
|
|
418
|
+
padding: '10px 14px',
|
|
419
|
+
borderRadius: 0,
|
|
420
|
+
minWidth: 0,
|
|
421
|
+
overflow: 'hidden',
|
|
422
|
+
border: '1px dashed #1f2937',
|
|
423
|
+
backgroundColor: '#0d1117',
|
|
424
|
+
fontFamily: theme.fonts.body,
|
|
425
|
+
fontSize: 12,
|
|
426
|
+
}}
|
|
427
|
+
>
|
|
428
|
+
<span style={{ color: '#4b5563', fontStyle: 'italic' }}>no raw payload</span>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const body = ((): React.ReactNode => {
|
|
434
|
+
switch (event.type) {
|
|
435
|
+
case 'session.created.1': {
|
|
436
|
+
const info = d.info as AnyRecord | undefined;
|
|
437
|
+
return (
|
|
438
|
+
<MetaRows
|
|
439
|
+
rows={[
|
|
440
|
+
['agent', info?.agent as string],
|
|
441
|
+
['model', ((info?.model as AnyRecord | undefined)?.id as string) ?? ((info?.model as AnyRecord | undefined)?.modelID as string)],
|
|
442
|
+
['directory', info?.directory as string],
|
|
443
|
+
['version', info?.version as string],
|
|
444
|
+
['slug', info?.slug as string],
|
|
445
|
+
]}
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
case 'session.updated.1': {
|
|
450
|
+
const info = d.info as AnyRecord | undefined;
|
|
451
|
+
return (
|
|
452
|
+
<MetaRows
|
|
453
|
+
rows={[
|
|
454
|
+
['cost', typeof info?.cost === 'number' ? `$${info.cost.toFixed(4)}` : undefined],
|
|
455
|
+
['tokens', info?.tokens as AnyRecord | undefined],
|
|
456
|
+
['title', info?.title as string],
|
|
457
|
+
]}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
case 'message.updated.1': {
|
|
462
|
+
const info = d.info as AnyRecord | undefined;
|
|
463
|
+
return (
|
|
464
|
+
<MetaRows
|
|
465
|
+
rows={[
|
|
466
|
+
['role', info?.role as string],
|
|
467
|
+
['agent', info?.agent as string],
|
|
468
|
+
['model', info?.model as AnyRecord | undefined],
|
|
469
|
+
['tokens', info?.tokens as AnyRecord | undefined],
|
|
470
|
+
['messageID', info?.id as string],
|
|
471
|
+
]}
|
|
472
|
+
/>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
case 'message.removed.1':
|
|
476
|
+
return <MetaRows rows={[['messageID', d.messageID as string]]} />;
|
|
477
|
+
case 'message.part.updated.1': {
|
|
478
|
+
switch (part?.type) {
|
|
479
|
+
case 'text':
|
|
480
|
+
case 'reasoning':
|
|
481
|
+
return <TextView text={part?.text as string | undefined} />;
|
|
482
|
+
case 'tool': {
|
|
483
|
+
const todos = (state?.input as AnyRecord | undefined)?.['todos'];
|
|
484
|
+
return (
|
|
485
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
486
|
+
<div style={{ fontSize: 12, color: '#9ca3af' }}>
|
|
487
|
+
callID {String(part?.callID ?? '')} · {String(state?.status ?? '')}
|
|
488
|
+
</div>
|
|
489
|
+
{Array.isArray(todos) ? <TodosView todos={todos} /> : null}
|
|
490
|
+
<ToolIO
|
|
491
|
+
input={state?.input}
|
|
492
|
+
output={state?.status === 'error' ? state?.error : state?.output}
|
|
493
|
+
/>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
case 'step-start':
|
|
498
|
+
return <MetaRows rows={[['snapshot', part?.snapshot as string]]} />;
|
|
499
|
+
case 'step-finish':
|
|
500
|
+
return (
|
|
501
|
+
<MetaRows
|
|
502
|
+
rows={[
|
|
503
|
+
['reason', part?.reason as string],
|
|
504
|
+
['cost', typeof part?.cost === 'number' ? `$${part.cost.toFixed(4)}` : undefined],
|
|
505
|
+
['tokens', part?.tokens as AnyRecord | undefined],
|
|
506
|
+
]}
|
|
507
|
+
/>
|
|
508
|
+
);
|
|
509
|
+
case 'patch':
|
|
510
|
+
return (
|
|
511
|
+
<MetaRows
|
|
512
|
+
rows={[
|
|
513
|
+
['files', part?.files as string[] | undefined],
|
|
514
|
+
['hash', part?.hash as string],
|
|
515
|
+
]}
|
|
516
|
+
/>
|
|
517
|
+
);
|
|
518
|
+
case 'compaction':
|
|
519
|
+
return (
|
|
520
|
+
<MetaRows
|
|
521
|
+
rows={[
|
|
522
|
+
['auto', typeof part?.auto === 'boolean' ? String(part.auto) : undefined],
|
|
523
|
+
['overflow', typeof part?.overflow === 'boolean' ? String(part.overflow) : undefined],
|
|
524
|
+
]}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
default:
|
|
528
|
+
return <JsonView value={d} />;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
default:
|
|
532
|
+
// Durable-agent raw blobs (Cursor store.db, etc.) have no V1 `data`
|
|
533
|
+
// envelope — dump the whole payload so the expanded card isn't empty.
|
|
534
|
+
return <JsonView value={isV1RawEvent(event) ? d : event} />;
|
|
535
|
+
}
|
|
536
|
+
})();
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<div
|
|
540
|
+
style={{
|
|
541
|
+
display: 'flex',
|
|
542
|
+
flexDirection: 'column',
|
|
543
|
+
gap: expanded ? 8 : 0,
|
|
544
|
+
padding: '10px 14px',
|
|
545
|
+
borderRadius: 0,
|
|
546
|
+
minWidth: 0,
|
|
547
|
+
overflow: 'hidden',
|
|
548
|
+
backgroundColor: expanded ? '#111827' : '#161b26',
|
|
549
|
+
fontFamily: theme.fonts.body,
|
|
550
|
+
fontSize: 13,
|
|
551
|
+
}}
|
|
552
|
+
>
|
|
553
|
+
<button
|
|
554
|
+
onClick={onToggle}
|
|
555
|
+
aria-expanded={expanded}
|
|
556
|
+
style={{
|
|
557
|
+
display: 'flex',
|
|
558
|
+
alignItems: 'center',
|
|
559
|
+
gap: 8,
|
|
560
|
+
width: '100%',
|
|
561
|
+
border: 'none',
|
|
562
|
+
background: 'none',
|
|
563
|
+
padding: 0,
|
|
564
|
+
cursor: 'pointer',
|
|
565
|
+
fontFamily: 'inherit',
|
|
566
|
+
textAlign: 'left',
|
|
567
|
+
}}
|
|
568
|
+
>
|
|
569
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{event.seq}</span>
|
|
570
|
+
<span
|
|
571
|
+
style={{
|
|
572
|
+
fontSize: 11,
|
|
573
|
+
fontWeight: 600,
|
|
574
|
+
textTransform: 'uppercase',
|
|
575
|
+
letterSpacing: 0.5,
|
|
576
|
+
color,
|
|
577
|
+
whiteSpace: 'nowrap',
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
{kindLabel(kind)}
|
|
581
|
+
</span>
|
|
582
|
+
{runCount && runCount > 1 ? (
|
|
583
|
+
<span
|
|
584
|
+
title={`${runCount} separate runs of this command`}
|
|
585
|
+
style={{
|
|
586
|
+
fontSize: 11,
|
|
587
|
+
fontWeight: 700,
|
|
588
|
+
color: '#f59e0b',
|
|
589
|
+
whiteSpace: 'nowrap',
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
×{runCount}
|
|
593
|
+
</span>
|
|
594
|
+
) : null}
|
|
595
|
+
{snapshotCount && snapshotCount > 1 ? (
|
|
596
|
+
<span
|
|
597
|
+
title={`${snapshotCount} raw event snapshots collapsed into this row`}
|
|
598
|
+
style={{
|
|
599
|
+
fontSize: 11,
|
|
600
|
+
fontWeight: 600,
|
|
601
|
+
color: '#60a5fa',
|
|
602
|
+
whiteSpace: 'nowrap',
|
|
603
|
+
}}
|
|
604
|
+
>
|
|
605
|
+
{snapshotCount} events
|
|
606
|
+
</span>
|
|
607
|
+
) : null}
|
|
608
|
+
<span
|
|
609
|
+
style={{
|
|
610
|
+
flex: 1,
|
|
611
|
+
overflow: 'hidden',
|
|
612
|
+
textOverflow: 'ellipsis',
|
|
613
|
+
whiteSpace: 'nowrap',
|
|
614
|
+
color: '#e5e7eb',
|
|
615
|
+
}}
|
|
616
|
+
>
|
|
617
|
+
{describe(event)}
|
|
618
|
+
</span>
|
|
619
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
|
|
620
|
+
{formatTime(eventTimestamp(event))}
|
|
621
|
+
</span>
|
|
622
|
+
</button>
|
|
623
|
+
{expanded ? body : null}
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function prettifyEventType(t: string | undefined): string {
|
|
629
|
+
return (t ?? '').replace(/-/g, ' ');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function normalizedDescribe(n: NormalizedFeedEvent): string {
|
|
633
|
+
if (n.toolName) return [n.toolName, n.operation].filter(Boolean).join(' · ');
|
|
634
|
+
if (n.operation) return n.operation;
|
|
635
|
+
const d = n.data as AnyRecord | undefined;
|
|
636
|
+
if (typeof d?.message === 'string') return d.message.slice(0, 90);
|
|
637
|
+
return prettifyEventType(n.eventType);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function repoLabel(repo: {
|
|
641
|
+
root?: string;
|
|
642
|
+
owner?: string;
|
|
643
|
+
repo?: string;
|
|
644
|
+
remoteUrl?: string;
|
|
645
|
+
}): string {
|
|
646
|
+
const identity = repo.owner && repo.repo ? `${repo.owner}/${repo.repo}` : repo.repo ?? '';
|
|
647
|
+
return identity && repo.root ? `${identity} @ ${repo.root}` : repo.root ?? '';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function NormalizedEventCard({
|
|
651
|
+
event,
|
|
652
|
+
seq,
|
|
653
|
+
color,
|
|
654
|
+
expanded,
|
|
655
|
+
onToggle,
|
|
656
|
+
}: {
|
|
657
|
+
event: NormalizedFeedEvent;
|
|
658
|
+
seq: number;
|
|
659
|
+
color: string;
|
|
660
|
+
expanded: boolean;
|
|
661
|
+
onToggle: () => void;
|
|
662
|
+
}) {
|
|
663
|
+
const { theme } = useTheme();
|
|
664
|
+
return (
|
|
665
|
+
<div
|
|
666
|
+
style={{
|
|
667
|
+
display: 'flex',
|
|
668
|
+
flexDirection: 'column',
|
|
669
|
+
gap: expanded ? 8 : 0,
|
|
670
|
+
padding: '10px 14px',
|
|
671
|
+
borderRadius: 0,
|
|
672
|
+
minWidth: 0,
|
|
673
|
+
overflow: 'hidden',
|
|
674
|
+
backgroundColor: expanded ? '#0d1520' : '#101722',
|
|
675
|
+
fontFamily: theme.fonts.body,
|
|
676
|
+
fontSize: 13,
|
|
677
|
+
}}
|
|
678
|
+
>
|
|
679
|
+
<button
|
|
680
|
+
onClick={onToggle}
|
|
681
|
+
aria-expanded={expanded}
|
|
682
|
+
style={{
|
|
683
|
+
display: 'flex',
|
|
684
|
+
alignItems: 'center',
|
|
685
|
+
gap: 8,
|
|
686
|
+
width: '100%',
|
|
687
|
+
border: 'none',
|
|
688
|
+
background: 'none',
|
|
689
|
+
padding: 0,
|
|
690
|
+
cursor: 'pointer',
|
|
691
|
+
fontFamily: 'inherit',
|
|
692
|
+
textAlign: 'left',
|
|
693
|
+
}}
|
|
694
|
+
>
|
|
695
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
|
|
696
|
+
<span
|
|
697
|
+
style={{
|
|
698
|
+
fontSize: 11,
|
|
699
|
+
fontWeight: 600,
|
|
700
|
+
textTransform: 'uppercase',
|
|
701
|
+
letterSpacing: 0.5,
|
|
702
|
+
color,
|
|
703
|
+
whiteSpace: 'nowrap',
|
|
704
|
+
}}
|
|
705
|
+
>
|
|
706
|
+
{prettifyEventType(event.eventType)}
|
|
707
|
+
</span>
|
|
708
|
+
<span
|
|
709
|
+
style={{
|
|
710
|
+
flex: 1,
|
|
711
|
+
overflow: 'hidden',
|
|
712
|
+
textOverflow: 'ellipsis',
|
|
713
|
+
whiteSpace: 'nowrap',
|
|
714
|
+
color: '#e5e7eb',
|
|
715
|
+
}}
|
|
716
|
+
>
|
|
717
|
+
{normalizedDescribe(event)}
|
|
718
|
+
</span>
|
|
719
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
|
|
720
|
+
{formatTime(event.timestamp)}
|
|
721
|
+
</span>
|
|
722
|
+
</button>
|
|
723
|
+
{expanded ? (
|
|
724
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
725
|
+
<MetaRows
|
|
726
|
+
rows={[
|
|
727
|
+
['eventType', event.eventType],
|
|
728
|
+
['operation', event.operation],
|
|
729
|
+
['toolName', event.toolName],
|
|
730
|
+
['sessionId', event.sessionId],
|
|
731
|
+
['workingDir', event.normalizedWorkingDirectory],
|
|
732
|
+
['repository', event.repository ? repoLabel(event.repository) : undefined],
|
|
733
|
+
]}
|
|
734
|
+
/>
|
|
735
|
+
{event.files && event.files.length > 0 ? (
|
|
736
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
737
|
+
<div style={{ fontSize: 11, color: '#6b7280' }}>files ({event.files.length})</div>
|
|
738
|
+
{event.files.map((file, i) => (
|
|
739
|
+
<FileRow key={i} file={file} />
|
|
740
|
+
))}
|
|
741
|
+
</div>
|
|
742
|
+
) : null}
|
|
743
|
+
{event.toolInput !== undefined ? (
|
|
744
|
+
<ToolIO input={event.toolInput} output={event.toolOutput} />
|
|
745
|
+
) : null}
|
|
746
|
+
</div>
|
|
747
|
+
) : null}
|
|
748
|
+
</div>
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function FileRow({ file }: { file: NormalizedPathInfo }) {
|
|
753
|
+
const { theme } = useTheme();
|
|
754
|
+
return (
|
|
755
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
756
|
+
<div
|
|
757
|
+
style={{
|
|
758
|
+
color: '#93c5fd',
|
|
759
|
+
fontSize: 12,
|
|
760
|
+
wordBreak: 'break-word',
|
|
761
|
+
fontFamily: theme.fonts.monospace,
|
|
762
|
+
}}
|
|
763
|
+
>
|
|
764
|
+
{file.displayPath}
|
|
765
|
+
</div>
|
|
766
|
+
<div style={{ color: '#6b7280', fontSize: 11, wordBreak: 'break-word' }}>
|
|
767
|
+
{file.context}
|
|
768
|
+
{file.repository
|
|
769
|
+
? ` · ${file.repository.gitRoot} → ${file.repository.relativePath}`
|
|
770
|
+
: ` · ${file.absolutePath}`}
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const OPERATION_COLORS: Record<string, string> = {
|
|
777
|
+
starting: '#6b7280',
|
|
778
|
+
prompting: '#a78bfa',
|
|
779
|
+
reading: '#a855f7',
|
|
780
|
+
grepping: '#e879f9',
|
|
781
|
+
globbing: '#14b8a6',
|
|
782
|
+
listing: '#f97316',
|
|
783
|
+
editing: '#22c55e',
|
|
784
|
+
tool: '#3b82f6',
|
|
785
|
+
errored: '#ef4444',
|
|
786
|
+
waiting: '#9ca3af',
|
|
787
|
+
finished: '#10b981',
|
|
788
|
+
compacting: '#ec4899',
|
|
789
|
+
subagent: '#f59e0b',
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
function AccumulatedEventCard({
|
|
793
|
+
event,
|
|
794
|
+
seq,
|
|
795
|
+
dropReason,
|
|
796
|
+
expanded,
|
|
797
|
+
onToggle,
|
|
798
|
+
}: {
|
|
799
|
+
event: AgentSessionEvent | null;
|
|
800
|
+
seq: number;
|
|
801
|
+
dropReason?: string;
|
|
802
|
+
expanded: boolean;
|
|
803
|
+
onToggle: () => void;
|
|
804
|
+
}) {
|
|
805
|
+
const { theme } = useTheme();
|
|
806
|
+
if (!event) {
|
|
807
|
+
return (
|
|
808
|
+
<div
|
|
809
|
+
style={{
|
|
810
|
+
display: 'flex',
|
|
811
|
+
alignItems: 'center',
|
|
812
|
+
gap: 8,
|
|
813
|
+
padding: '10px 14px',
|
|
814
|
+
borderRadius: 0,
|
|
815
|
+
minWidth: 0,
|
|
816
|
+
overflow: 'hidden',
|
|
817
|
+
border: '1px dashed #1f2937',
|
|
818
|
+
backgroundColor: '#0d1117',
|
|
819
|
+
fontFamily: theme.fonts.body,
|
|
820
|
+
fontSize: 12,
|
|
821
|
+
}}
|
|
822
|
+
>
|
|
823
|
+
<span style={{ color: '#374151', whiteSpace: 'nowrap' }}>#{seq}</span>
|
|
824
|
+
<span style={{ color: dropReason ? '#b45309' : '#4b5563', fontStyle: 'italic' }}>
|
|
825
|
+
{dropReason ?? 'no UI event (accumulator dropped)'}
|
|
826
|
+
</span>
|
|
827
|
+
</div>
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const opColor = OPERATION_COLORS[event.operation] ?? '#6b7280';
|
|
832
|
+
|
|
833
|
+
return (
|
|
834
|
+
<div
|
|
835
|
+
style={{
|
|
836
|
+
display: 'flex',
|
|
837
|
+
flexDirection: 'column',
|
|
838
|
+
gap: expanded ? 8 : 0,
|
|
839
|
+
padding: '10px 14px',
|
|
840
|
+
borderRadius: 0,
|
|
841
|
+
minWidth: 0,
|
|
842
|
+
overflow: 'hidden',
|
|
843
|
+
backgroundColor: expanded ? '#121a13' : '#141b16',
|
|
844
|
+
fontFamily: theme.fonts.body,
|
|
845
|
+
fontSize: 13,
|
|
846
|
+
}}
|
|
847
|
+
>
|
|
848
|
+
<button
|
|
849
|
+
onClick={onToggle}
|
|
850
|
+
aria-expanded={expanded}
|
|
851
|
+
style={{
|
|
852
|
+
display: 'flex',
|
|
853
|
+
alignItems: 'center',
|
|
854
|
+
gap: 8,
|
|
855
|
+
width: '100%',
|
|
856
|
+
border: 'none',
|
|
857
|
+
background: 'none',
|
|
858
|
+
padding: 0,
|
|
859
|
+
cursor: 'pointer',
|
|
860
|
+
fontFamily: 'inherit',
|
|
861
|
+
textAlign: 'left',
|
|
862
|
+
}}
|
|
863
|
+
>
|
|
864
|
+
<span
|
|
865
|
+
style={{
|
|
866
|
+
width: 8,
|
|
867
|
+
height: 8,
|
|
868
|
+
borderRadius: '50%',
|
|
869
|
+
flexShrink: 0,
|
|
870
|
+
backgroundColor: event.sessionColor,
|
|
871
|
+
}}
|
|
872
|
+
/>
|
|
873
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
|
|
874
|
+
<span
|
|
875
|
+
style={{
|
|
876
|
+
fontSize: 11,
|
|
877
|
+
fontWeight: 600,
|
|
878
|
+
textTransform: 'uppercase',
|
|
879
|
+
letterSpacing: 0.5,
|
|
880
|
+
color: opColor,
|
|
881
|
+
whiteSpace: 'nowrap',
|
|
882
|
+
}}
|
|
883
|
+
>
|
|
884
|
+
{event.operation}
|
|
885
|
+
</span>
|
|
886
|
+
<span
|
|
887
|
+
style={{
|
|
888
|
+
flex: 1,
|
|
889
|
+
overflow: 'hidden',
|
|
890
|
+
textOverflow: 'ellipsis',
|
|
891
|
+
whiteSpace: 'nowrap',
|
|
892
|
+
color: '#e5e7eb',
|
|
893
|
+
}}
|
|
894
|
+
>
|
|
895
|
+
{event.description}
|
|
896
|
+
</span>
|
|
897
|
+
<span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
|
|
898
|
+
{formatTime(event.timestamp)}
|
|
899
|
+
</span>
|
|
900
|
+
</button>
|
|
901
|
+
{expanded ? (
|
|
902
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
903
|
+
<MetaRows
|
|
904
|
+
rows={[
|
|
905
|
+
['operation', event.operation],
|
|
906
|
+
['session', event.sessionName],
|
|
907
|
+
['contextTokens', event.contextTokens],
|
|
908
|
+
['toolName', event.toolName],
|
|
909
|
+
['subagent', event.subagentType],
|
|
910
|
+
['childSessionId', event.childSessionId],
|
|
911
|
+
]}
|
|
912
|
+
/>
|
|
913
|
+
<div style={{ fontSize: 12, color: '#6b7280' }}>{event.description}</div>
|
|
914
|
+
{event.files.length > 0 ? <PathList label="files" paths={event.files} /> : null}
|
|
915
|
+
{event.dependencies.length > 0 ? (
|
|
916
|
+
<PathList label="dependencies" paths={event.dependencies} />
|
|
917
|
+
) : null}
|
|
918
|
+
{event.layers.length > 0 ? (
|
|
919
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
920
|
+
<div style={{ fontSize: 11, color: '#6b7280' }}>layers</div>
|
|
921
|
+
{event.layers.map((layer) => (
|
|
922
|
+
<div
|
|
923
|
+
key={layer.id}
|
|
924
|
+
style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12 }}
|
|
925
|
+
>
|
|
926
|
+
<span
|
|
927
|
+
style={{
|
|
928
|
+
width: 8,
|
|
929
|
+
height: 8,
|
|
930
|
+
borderRadius: '50%',
|
|
931
|
+
backgroundColor: layer.color,
|
|
932
|
+
opacity: 0.8,
|
|
933
|
+
}}
|
|
934
|
+
/>
|
|
935
|
+
<span style={{ color: '#d1d5db' }}>
|
|
936
|
+
{layer.name} ({layer.items.length})
|
|
937
|
+
</span>
|
|
938
|
+
</div>
|
|
939
|
+
))}
|
|
940
|
+
</div>
|
|
941
|
+
) : null}
|
|
942
|
+
</div>
|
|
943
|
+
) : null}
|
|
944
|
+
</div>
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function PathList({ label, paths }: { label: string; paths: NormalizedPathInfo[] }) {
|
|
949
|
+
const { theme } = useTheme();
|
|
950
|
+
return (
|
|
951
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
952
|
+
<div style={{ fontSize: 11, color: '#6b7280' }}>{label}</div>
|
|
953
|
+
{paths.map((p, i) => (
|
|
954
|
+
<div
|
|
955
|
+
key={i}
|
|
956
|
+
style={{ color: '#93c5fd', fontSize: 11.5, fontFamily: theme.fonts.monospace, wordBreak: 'break-word' }}
|
|
957
|
+
>
|
|
958
|
+
{p.displayPath}
|
|
959
|
+
</div>
|
|
960
|
+
))}
|
|
961
|
+
</div>
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function TextView({ text }: { text?: string }) {
|
|
966
|
+
if (!text) return <div style={{ color: '#6b7280', fontSize: 12 }}>(empty)</div>;
|
|
967
|
+
return (
|
|
968
|
+
<div
|
|
969
|
+
style={{
|
|
970
|
+
color: '#d1d5db',
|
|
971
|
+
fontSize: 12.5,
|
|
972
|
+
lineHeight: 1.5,
|
|
973
|
+
maxHeight: 120,
|
|
974
|
+
overflowY: 'auto',
|
|
975
|
+
whiteSpace: 'pre-wrap',
|
|
976
|
+
wordBreak: 'break-word',
|
|
977
|
+
}}
|
|
978
|
+
>
|
|
979
|
+
{text}
|
|
980
|
+
</div>
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const TODO_STATUS_COLORS: Record<string, string> = {
|
|
985
|
+
completed: '#22c55e',
|
|
986
|
+
in_progress: '#f59e0b',
|
|
987
|
+
pending: '#6b7280',
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
function TodosView({ todos }: { todos: Array<Record<string, unknown>> }) {
|
|
991
|
+
return (
|
|
992
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
993
|
+
<div style={{ fontSize: 11, color: '#6b7280' }}>todos ({todos.length})</div>
|
|
994
|
+
{todos.map((t, i) => (
|
|
995
|
+
<div key={i} style={{ display: 'flex', gap: 8, fontSize: 12, alignItems: 'baseline' }}>
|
|
996
|
+
<span
|
|
997
|
+
style={{
|
|
998
|
+
color: TODO_STATUS_COLORS[String(t.status ?? '')] ?? '#6b7280',
|
|
999
|
+
fontWeight: 600,
|
|
1000
|
+
minWidth: 84,
|
|
1001
|
+
whiteSpace: 'nowrap',
|
|
1002
|
+
}}
|
|
1003
|
+
>
|
|
1004
|
+
{String(t.status ?? '')}
|
|
1005
|
+
</span>
|
|
1006
|
+
<span style={{ color: '#d1d5db', wordBreak: 'break-word' }}>{String(t.content ?? '')}</span>
|
|
1007
|
+
{typeof t.priority === 'string' && t.priority ? (
|
|
1008
|
+
<span style={{ color: '#6b7280', whiteSpace: 'nowrap' }}>· {t.priority}</span>
|
|
1009
|
+
) : null}
|
|
1010
|
+
</div>
|
|
1011
|
+
))}
|
|
1012
|
+
</div>
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function ToolIO({ input, output }: { input?: unknown; output?: unknown }) { const summary = toolSummary(input);
|
|
1017
|
+
return (
|
|
1018
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
1019
|
+
{summary ? (
|
|
1020
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
1021
|
+
<span style={{ fontSize: 11, color: '#60a5fa', fontWeight: 600 }}>IN</span>
|
|
1022
|
+
<code style={{ color: '#93c5fd', fontSize: 12, wordBreak: 'break-word' }}>{summary}</code>
|
|
1023
|
+
</div>
|
|
1024
|
+
) : null}
|
|
1025
|
+
{input !== undefined && input !== null && typeof input === 'object' ? (
|
|
1026
|
+
<CodeBlock value={JSON.stringify(input, null, 2)} />
|
|
1027
|
+
) : null}
|
|
1028
|
+
{output !== undefined && output !== null ? (
|
|
1029
|
+
<>
|
|
1030
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
1031
|
+
<span style={{ fontSize: 11, color: '#34d399', fontWeight: 600 }}>OUT</span>
|
|
1032
|
+
<span style={{ fontSize: 11, color: '#6b7280' }}>
|
|
1033
|
+
{typeof output === 'string' ? `${output.length.toLocaleString()} chars` : 'object'}
|
|
1034
|
+
</span>
|
|
1035
|
+
</div>
|
|
1036
|
+
<CodeBlock value={typeof output === 'string' ? output : JSON.stringify(output, null, 2)} />
|
|
1037
|
+
</>
|
|
1038
|
+
) : null}
|
|
1039
|
+
</div>
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function CodeBlock({ value }: { value: string }) {
|
|
1044
|
+
return (
|
|
1045
|
+
<pre
|
|
1046
|
+
style={{
|
|
1047
|
+
margin: 0,
|
|
1048
|
+
padding: 8,
|
|
1049
|
+
borderRadius: 6,
|
|
1050
|
+
backgroundColor: '#0b1220',
|
|
1051
|
+
color: '#9ca3af',
|
|
1052
|
+
fontSize: 11.5,
|
|
1053
|
+
lineHeight: 1.45,
|
|
1054
|
+
maxHeight: 180,
|
|
1055
|
+
overflow: 'auto',
|
|
1056
|
+
whiteSpace: 'pre-wrap',
|
|
1057
|
+
wordBreak: 'break-word',
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
{value}
|
|
1061
|
+
</pre>
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function MetaRows({ rows }: { rows: Array<[string, unknown]> }) {
|
|
1066
|
+
const present = rows.filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
1067
|
+
if (present.length === 0) return null;
|
|
1068
|
+
return (
|
|
1069
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
1070
|
+
{present.map(([label, value]) => (
|
|
1071
|
+
<div key={label} style={{ display: 'flex', gap: 8, fontSize: 12 }}>
|
|
1072
|
+
<span style={{ color: '#6b7280', minWidth: 76, whiteSpace: 'nowrap' }}>{label}</span>
|
|
1073
|
+
<span
|
|
1074
|
+
style={{
|
|
1075
|
+
color: '#d1d5db',
|
|
1076
|
+
wordBreak: 'break-word',
|
|
1077
|
+
whiteSpace: typeof value === 'string' ? 'pre-wrap' : 'nowrap',
|
|
1078
|
+
}}
|
|
1079
|
+
>
|
|
1080
|
+
{typeof value === 'string'
|
|
1081
|
+
? value
|
|
1082
|
+
: Array.isArray(value)
|
|
1083
|
+
? (value as unknown[]).join(', ')
|
|
1084
|
+
: JSON.stringify(value)}
|
|
1085
|
+
</span>
|
|
1086
|
+
</div>
|
|
1087
|
+
))}
|
|
1088
|
+
</div>
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function JsonView({ value }: { value: unknown }) {
|
|
1093
|
+
return <CodeBlock value={JSON.stringify(value, null, 2)} />;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// =============================================================================
|
|
1097
|
+
// Feed
|
|
1098
|
+
// =============================================================================
|
|
1099
|
+
|
|
1100
|
+
type FeedItem = SessionEventFeedRow & {
|
|
1101
|
+
/** Distinct invocations (callIDs) merged into this row. */
|
|
1102
|
+
runCount?: number;
|
|
1103
|
+
/** Raw event snapshots collapsed into this row (pending/running/completed…). */
|
|
1104
|
+
snapshotCount?: number;
|
|
1105
|
+
/** Why the accumulator dropped this row's UI event, when it did. */
|
|
1106
|
+
dropReason?: string;
|
|
1107
|
+
/** CallIDs folded into this row — the accumulator emits at most one UI event
|
|
1108
|
+
* per invocation, so this stays the source of truth for `runCount`. */
|
|
1109
|
+
callIDs?: Set<string>;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Why the accumulator dropped a row's UI event, derived from the raw event
|
|
1114
|
+
* alone (no stream context needed). Rows that don't match a known category keep
|
|
1115
|
+
* the generic "dropped" label.
|
|
1116
|
+
*/
|
|
1117
|
+
function dropReasonFor(raw: V1RawEvent | null): string | null {
|
|
1118
|
+
if (!raw) return null;
|
|
1119
|
+
if (raw.type === 'message.updated.1') {
|
|
1120
|
+
const role = (dataOf(raw).info as AnyRecord | undefined)?.role;
|
|
1121
|
+
if (role === 'assistant') {
|
|
1122
|
+
return 'assistant message — metadata only (feeds context/token tracking)';
|
|
1123
|
+
}
|
|
1124
|
+
return null; // user messages are handled by the re-save pass below
|
|
1125
|
+
}
|
|
1126
|
+
if (raw.type === 'message.removed.1') return 'message removed — no UI event';
|
|
1127
|
+
if (raw.type === 'message.part.updated.1') {
|
|
1128
|
+
switch (partOf(raw)?.type) {
|
|
1129
|
+
case 'reasoning':
|
|
1130
|
+
return 'model reasoning — not surfaced in the UI';
|
|
1131
|
+
case 'step-start':
|
|
1132
|
+
case 'step-finish':
|
|
1133
|
+
return 'step lifecycle — not surfaced in the UI';
|
|
1134
|
+
case 'text':
|
|
1135
|
+
return 'text content — not surfaced in the UI';
|
|
1136
|
+
case 'tool':
|
|
1137
|
+
return 'tool snapshot — folded into the tool run';
|
|
1138
|
+
default:
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Annotate rows the accumulator drops with *why* they were dropped, walking in
|
|
1147
|
+
* stream order. opencode re-saves a user message on every change (seq 1, 6, 24,
|
|
1148
|
+
* 39, 51… share one messageID), so the first occurrence becomes the `prompting`
|
|
1149
|
+
* UI event and every re-save is deduped away — those read "re-save, ignored"
|
|
1150
|
+
* instead of a generic "dropped".
|
|
1151
|
+
*/
|
|
1152
|
+
function annotateDrops(items: FeedItem[]): FeedItem[] {
|
|
1153
|
+
const seenUserMessageIDs = new Set<string>();
|
|
1154
|
+
return items.map((item) => {
|
|
1155
|
+
const raw = toRaw(item.raw);
|
|
1156
|
+
if (!raw) return item;
|
|
1157
|
+
// Track user message IDs on every occurrence — the first one is the real
|
|
1158
|
+
// prompt (it carries the accumulated UI event); later re-saves are deduped.
|
|
1159
|
+
if (raw.type === 'message.updated.1') {
|
|
1160
|
+
const info = dataOf(raw).info as AnyRecord | undefined;
|
|
1161
|
+
const messageID = info?.id;
|
|
1162
|
+
const role = info?.role;
|
|
1163
|
+
if (typeof messageID === 'string' && role === 'user') {
|
|
1164
|
+
if (seenUserMessageIDs.has(messageID)) {
|
|
1165
|
+
if (item.accumulated === null) {
|
|
1166
|
+
return {
|
|
1167
|
+
...item,
|
|
1168
|
+
dropReason: 're-save of user message — already counted as a prompt',
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return item;
|
|
1172
|
+
}
|
|
1173
|
+
seenUserMessageIDs.add(messageID);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (item.accumulated !== null) return item;
|
|
1177
|
+
const reason = dropReasonFor(raw);
|
|
1178
|
+
if (reason) return { ...item, dropReason: reason };
|
|
1179
|
+
return item;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Collapse a tool invocation's snapshots into a single row. The raw V1 stream
|
|
1185
|
+
* emits one part update per status flip (pending → running → completed) — and
|
|
1186
|
+
* those flips can be interleaved with other events (reasoning, text, session
|
|
1187
|
+
* bookkeeping) — so snapshots are matched by callID across the whole stream,
|
|
1188
|
+
* not just when consecutive. Distinct calls that repeat the same command and
|
|
1189
|
+
* are consecutive still fold into one ×N row.
|
|
1190
|
+
*
|
|
1191
|
+
* The merged row keeps the run's first seq as a stable key and adopts the
|
|
1192
|
+
* **latest** snapshot's raw/normalized payload (the completed/errored part
|
|
1193
|
+
* carries the tool output). The UI (accumulated) column instead prefers the
|
|
1194
|
+
* group's one non-null accumulated event — the accumulator dedupes to at most
|
|
1195
|
+
* one per invocation, and it's usually the *first* snapshot (e.g. todowrite's
|
|
1196
|
+
* pending emits the `tool` event; running/completed are dropped).
|
|
1197
|
+
*/
|
|
1198
|
+
function groupToolRuns(items: SessionEventFeedRow[]): FeedItem[] {
|
|
1199
|
+
const out: FeedItem[] = [];
|
|
1200
|
+
const byCallID = new Map<string, FeedItem>();
|
|
1201
|
+
let lastGroup: FeedItem | null = null;
|
|
1202
|
+
let lastGroupCmd: string | null = null;
|
|
1203
|
+
|
|
1204
|
+
const adopt = (group: FeedItem, r: SessionEventFeedRow, cmd: string | null) => {
|
|
1205
|
+
group.snapshotCount = (group.snapshotCount ?? 1) + 1;
|
|
1206
|
+
group.raw = r.raw;
|
|
1207
|
+
group.normalized = r.normalized;
|
|
1208
|
+
const acc = r.accumulated;
|
|
1209
|
+
if (acc !== null) {
|
|
1210
|
+
// Prefer the run's meaningful accumulated. The pending snapshot of a
|
|
1211
|
+
// bash `ls` (empty input) emits a generic `tool` before the completed
|
|
1212
|
+
// snapshot's `listing` arrives — replace a generic `tool` with a more
|
|
1213
|
+
// specific operation, and never downgrade a specific one.
|
|
1214
|
+
if (
|
|
1215
|
+
group.accumulated === null ||
|
|
1216
|
+
(group.accumulated.operation === 'tool' && acc.operation !== 'tool')
|
|
1217
|
+
) {
|
|
1218
|
+
group.accumulated = acc;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
group.type = r.type;
|
|
1222
|
+
lastGroup = group;
|
|
1223
|
+
lastGroupCmd = cmd;
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
for (const r of items) {
|
|
1227
|
+
const raw = toRaw(r.raw);
|
|
1228
|
+
const isTool =
|
|
1229
|
+
!!raw && raw.type === 'message.part.updated.1' && partOf(raw)?.type === 'tool';
|
|
1230
|
+
const part = partOf(raw);
|
|
1231
|
+
const cmd = isTool
|
|
1232
|
+
? toolSummary((part?.state as AnyRecord | undefined)?.input)
|
|
1233
|
+
: null;
|
|
1234
|
+
const callID = isTool ? (part?.callID as string | undefined) : undefined;
|
|
1235
|
+
|
|
1236
|
+
if (!isTool) {
|
|
1237
|
+
out.push(r);
|
|
1238
|
+
lastGroup = null;
|
|
1239
|
+
lastGroupCmd = null;
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Same invocation — fold this status snapshot in wherever it appears.
|
|
1244
|
+
const existing = callID ? byCallID.get(callID) : undefined;
|
|
1245
|
+
if (existing) {
|
|
1246
|
+
adopt(existing, r, cmd);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// A distinct call repeating the previous group's command folds into it.
|
|
1251
|
+
if (lastGroup && cmd !== null && cmd !== '' && cmd === lastGroupCmd) {
|
|
1252
|
+
if (callID) {
|
|
1253
|
+
lastGroup.callIDs = lastGroup.callIDs ?? new Set<string>();
|
|
1254
|
+
lastGroup.callIDs.add(callID);
|
|
1255
|
+
byCallID.set(callID, lastGroup);
|
|
1256
|
+
}
|
|
1257
|
+
lastGroup.runCount = lastGroup.callIDs ? lastGroup.callIDs.size : 1;
|
|
1258
|
+
adopt(lastGroup, r, cmd);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const group: FeedItem = {
|
|
1263
|
+
...r,
|
|
1264
|
+
runCount: 1,
|
|
1265
|
+
snapshotCount: 1,
|
|
1266
|
+
callIDs: new Set(callID ? [callID] : []),
|
|
1267
|
+
};
|
|
1268
|
+
if (callID) byCallID.set(callID, group);
|
|
1269
|
+
out.push(group);
|
|
1270
|
+
lastGroup = group;
|
|
1271
|
+
lastGroupCmd = cmd;
|
|
1272
|
+
}
|
|
1273
|
+
return out;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Fold a user message's events into a single row keyed by messageID: the
|
|
1278
|
+
* original `message.updated.1` (which carries the `prompting` accumulated
|
|
1279
|
+
* event), every re-save of the same message (opencode re-saves a user message
|
|
1280
|
+
* on each change), and the text part(s) that carry its content (the
|
|
1281
|
+
* `notification` events sharing the same messageID). They all resolve to the
|
|
1282
|
+
* one `prompting` accumulated event, so they render as one row with a snapshot
|
|
1283
|
+
* count instead of a wall of "re-save" rows.
|
|
1284
|
+
*/
|
|
1285
|
+
function groupPromptParts(items: FeedItem[]): FeedItem[] {
|
|
1286
|
+
const out: FeedItem[] = [];
|
|
1287
|
+
const byMessageID = new Map<string, FeedItem>();
|
|
1288
|
+
for (const item of items) {
|
|
1289
|
+
const raw = toRaw(item.raw);
|
|
1290
|
+
if (!raw) {
|
|
1291
|
+
out.push(item);
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (raw.type === 'message.updated.1') {
|
|
1295
|
+
const info = dataOf(raw).info as AnyRecord | undefined;
|
|
1296
|
+
const messageID = info?.id;
|
|
1297
|
+
const role = info?.role;
|
|
1298
|
+
if (typeof messageID === 'string' && role === 'user') {
|
|
1299
|
+
const existing = byMessageID.get(messageID);
|
|
1300
|
+
if (existing) {
|
|
1301
|
+
// re-save of the same user message — fold into its prompt row
|
|
1302
|
+
existing.snapshotCount = (existing.snapshotCount ?? 1) + 1;
|
|
1303
|
+
if (existing.accumulated === null && item.accumulated !== null) {
|
|
1304
|
+
existing.accumulated = item.accumulated;
|
|
1305
|
+
}
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
byMessageID.set(messageID, item);
|
|
1309
|
+
}
|
|
1310
|
+
out.push(item);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (raw.type === 'message.part.updated.1') {
|
|
1314
|
+
const part = partOf(raw);
|
|
1315
|
+
const messageID = part?.messageID;
|
|
1316
|
+
if (part?.type === 'text' && typeof messageID === 'string') {
|
|
1317
|
+
const existing = byMessageID.get(messageID);
|
|
1318
|
+
if (existing) {
|
|
1319
|
+
existing.snapshotCount = (existing.snapshotCount ?? 1) + 1;
|
|
1320
|
+
if (existing.accumulated === null && item.accumulated !== null) {
|
|
1321
|
+
existing.accumulated = item.accumulated;
|
|
1322
|
+
}
|
|
1323
|
+
continue; // absorb the text part into its message row
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
out.push(item);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
out.push(item);
|
|
1330
|
+
}
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export function SessionEventFeed({ title, rows }: SessionEventFeedProps) {
|
|
1335
|
+
const { theme } = useTheme();
|
|
1336
|
+
const items = useMemo(() => [...rows].sort((a, b) => a.seq - b.seq), [rows]);
|
|
1337
|
+
|
|
1338
|
+
const [active, setActive] = useState<string>('activity');
|
|
1339
|
+
const [groupTools, setGroupTools] = useState(true);
|
|
1340
|
+
|
|
1341
|
+
const displayItems = useMemo<FeedItem[]>(
|
|
1342
|
+
() => annotateDrops(groupPromptParts(groupTools ? groupToolRuns(items) : items)),
|
|
1343
|
+
[items, groupTools],
|
|
1344
|
+
);
|
|
1345
|
+
const seqs = useMemo(() => displayItems.map((r) => r.seq), [displayItems]);
|
|
1346
|
+
const { collapsed, toggle, setAll } = useCollapse(seqs, true);
|
|
1347
|
+
|
|
1348
|
+
const activePreset = PRESETS.find((p) => p.id === active) ?? PRESETS[0];
|
|
1349
|
+
// "Generic tools" surfaces the improvement backlog: tool rows whose
|
|
1350
|
+
// accumulated operation is still the generic `tool` fallback (e.g. bash
|
|
1351
|
+
// commands that haven't been given a listing/reading/… type yet).
|
|
1352
|
+
const [unprocessedOnly, setUnprocessedOnly] = useState(false);
|
|
1353
|
+
const isVisible = (r: FeedItem) =>
|
|
1354
|
+
unprocessedOnly
|
|
1355
|
+
? r.accumulated?.operation === 'tool'
|
|
1356
|
+
: activePreset.include.includes(eventCategory(toRaw(r.raw)));
|
|
1357
|
+
const visibleEvents = displayItems.filter(isVisible);
|
|
1358
|
+
|
|
1359
|
+
const presetCounts = useMemo(() => {
|
|
1360
|
+
const map = new Map<string, number>();
|
|
1361
|
+
for (const preset of PRESETS) {
|
|
1362
|
+
map.set(preset.id, displayItems.filter((r) => preset.include.includes(eventCategory(toRaw(r.raw)))).length);
|
|
1363
|
+
}
|
|
1364
|
+
return map;
|
|
1365
|
+
}, [displayItems]);
|
|
1366
|
+
|
|
1367
|
+
const unprocessedCount = useMemo(
|
|
1368
|
+
() => displayItems.filter((r) => r.accumulated?.operation === 'tool').length,
|
|
1369
|
+
[displayItems],
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
const expandedCount = visibleEvents.filter((r) => !collapsed.has(r.seq)).length;
|
|
1373
|
+
|
|
1374
|
+
return (
|
|
1375
|
+
<div style={{ padding: 24, backgroundColor: '#0d1117', minHeight: '100vh' }}>
|
|
1376
|
+
<div
|
|
1377
|
+
style={{
|
|
1378
|
+
display: 'flex',
|
|
1379
|
+
alignItems: 'baseline',
|
|
1380
|
+
gap: 12,
|
|
1381
|
+
marginBottom: 4,
|
|
1382
|
+
fontFamily: theme.fonts.body,
|
|
1383
|
+
}}
|
|
1384
|
+
>
|
|
1385
|
+
<h1 style={{ margin: 0, color: '#f3f4f6', fontSize: 18, fontWeight: 600 }}>{title}</h1>
|
|
1386
|
+
<span style={{ color: '#6b7280', fontSize: 12 }}>
|
|
1387
|
+
{items.length} raw events · {visibleEvents.length} visible · {expandedCount} expanded
|
|
1388
|
+
</span>
|
|
1389
|
+
</div>
|
|
1390
|
+
<div
|
|
1391
|
+
style={{
|
|
1392
|
+
display: 'flex',
|
|
1393
|
+
gap: 6,
|
|
1394
|
+
flexWrap: 'wrap',
|
|
1395
|
+
marginBottom: 16,
|
|
1396
|
+
fontFamily: theme.fonts.body,
|
|
1397
|
+
}}
|
|
1398
|
+
>
|
|
1399
|
+
<FilterChip label="all" active={active === 'all'} onClick={() => setActive('all')} />
|
|
1400
|
+
{PRESETS.filter((p) => p.id !== 'all').map((preset) => (
|
|
1401
|
+
<FilterChip
|
|
1402
|
+
key={preset.id}
|
|
1403
|
+
label={`${preset.label} (${presetCounts.get(preset.id) ?? 0})`}
|
|
1404
|
+
active={active === preset.id}
|
|
1405
|
+
onClick={() => setActive(preset.id)}
|
|
1406
|
+
/>
|
|
1407
|
+
))}
|
|
1408
|
+
<span style={{ flex: 1 }} />
|
|
1409
|
+
<FilterChip
|
|
1410
|
+
label={`Generic tools (${unprocessedCount})`}
|
|
1411
|
+
active={unprocessedOnly}
|
|
1412
|
+
onClick={() => setUnprocessedOnly((v) => !v)}
|
|
1413
|
+
/>
|
|
1414
|
+
<FilterChip
|
|
1415
|
+
label={groupTools ? 'Group tool runs: on' : 'Group tool runs: off'}
|
|
1416
|
+
active={groupTools}
|
|
1417
|
+
onClick={() => setGroupTools((v) => !v)}
|
|
1418
|
+
/>
|
|
1419
|
+
<FilterChip label="Expand all" active={false} onClick={() => setAll(false)} />
|
|
1420
|
+
<FilterChip label="Collapse all" active={false} onClick={() => setAll(true)} />
|
|
1421
|
+
</div>
|
|
1422
|
+
<ColumnHeaders />
|
|
1423
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
1424
|
+
{displayItems.map((r) => (
|
|
1425
|
+
<EventRow
|
|
1426
|
+
key={r.seq}
|
|
1427
|
+
row={r}
|
|
1428
|
+
sessionTitle={title}
|
|
1429
|
+
runCount={r.runCount}
|
|
1430
|
+
snapshotCount={r.snapshotCount}
|
|
1431
|
+
visible={isVisible(r)}
|
|
1432
|
+
expanded={!collapsed.has(r.seq)}
|
|
1433
|
+
onToggle={() => toggle(r.seq)}
|
|
1434
|
+
/>
|
|
1435
|
+
))}
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
export function SessionEventFeedGrouped({ title, rows }: SessionEventFeedGroupedProps) {
|
|
1442
|
+
const { theme } = useTheme();
|
|
1443
|
+
const items = useMemo<FeedItem[]>(
|
|
1444
|
+
() => annotateDrops(groupPromptParts([...rows].sort((a, b) => a.seq - b.seq))),
|
|
1445
|
+
[rows],
|
|
1446
|
+
);
|
|
1447
|
+
const byKind = useMemo(() => {
|
|
1448
|
+
const map = new Map<string, SessionEventFeedRow[]>();
|
|
1449
|
+
for (const r of items) {
|
|
1450
|
+
const kind = eventKind(toRaw(r.raw));
|
|
1451
|
+
map.set(kind, [...(map.get(kind) ?? []), r]);
|
|
1452
|
+
}
|
|
1453
|
+
return map;
|
|
1454
|
+
}, [items]);
|
|
1455
|
+
|
|
1456
|
+
return (
|
|
1457
|
+
<div style={{ padding: 24, backgroundColor: '#0d1117', minHeight: '100vh' }}>
|
|
1458
|
+
{title ? (
|
|
1459
|
+
<h1 style={{ margin: '0 0 12px', color: '#f3f4f6', fontSize: 18, fontWeight: 600, fontFamily: theme.fonts.body }}>
|
|
1460
|
+
{title}
|
|
1461
|
+
</h1>
|
|
1462
|
+
) : null}
|
|
1463
|
+
<ColumnHeaders />
|
|
1464
|
+
{Array.from(byKind.entries())
|
|
1465
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
1466
|
+
.map(([kind, group]) => (
|
|
1467
|
+
<GroupSection key={kind} kind={kind} group={group} sessionTitle={title} />
|
|
1468
|
+
))}
|
|
1469
|
+
</div>
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function GroupSection({
|
|
1474
|
+
kind,
|
|
1475
|
+
group,
|
|
1476
|
+
sessionTitle,
|
|
1477
|
+
}: {
|
|
1478
|
+
kind: string;
|
|
1479
|
+
group: SessionEventFeedRow[];
|
|
1480
|
+
sessionTitle?: string;
|
|
1481
|
+
}) {
|
|
1482
|
+
const { theme } = useTheme();
|
|
1483
|
+
const seqs = useMemo(() => group.map((r) => r.seq), [group]);
|
|
1484
|
+
const { collapsed, toggle, setAll } = useCollapse(seqs, true);
|
|
1485
|
+
|
|
1486
|
+
return (
|
|
1487
|
+
<div style={{ marginBottom: 20 }}>
|
|
1488
|
+
<div
|
|
1489
|
+
style={{
|
|
1490
|
+
display: 'flex',
|
|
1491
|
+
alignItems: 'center',
|
|
1492
|
+
gap: 8,
|
|
1493
|
+
marginBottom: 8,
|
|
1494
|
+
fontFamily: theme.fonts.body,
|
|
1495
|
+
}}
|
|
1496
|
+
>
|
|
1497
|
+
<span style={{ color: kindColor(kind), fontWeight: 600, fontSize: 13 }}>
|
|
1498
|
+
{kindLabel(kind)}
|
|
1499
|
+
</span>
|
|
1500
|
+
<span style={{ color: '#6b7280', fontSize: 12 }}>{group.length}</span>
|
|
1501
|
+
<span style={{ flex: 1 }} />
|
|
1502
|
+
<FilterChip label="Expand all" active={false} onClick={() => setAll(false)} />
|
|
1503
|
+
<FilterChip label="Collapse all" active={false} onClick={() => setAll(true)} />
|
|
1504
|
+
</div>
|
|
1505
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
1506
|
+
{group.map((r) => (
|
|
1507
|
+
<EventRow
|
|
1508
|
+
key={r.seq}
|
|
1509
|
+
row={r}
|
|
1510
|
+
sessionTitle={sessionTitle}
|
|
1511
|
+
visible
|
|
1512
|
+
expanded={!collapsed.has(r.seq)}
|
|
1513
|
+
onToggle={() => toggle(r.seq)}
|
|
1514
|
+
/>
|
|
1515
|
+
))}
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function ColumnHeaders() {
|
|
1522
|
+
const { theme } = useTheme();
|
|
1523
|
+
return (
|
|
1524
|
+
<div
|
|
1525
|
+
style={{
|
|
1526
|
+
position: 'sticky',
|
|
1527
|
+
top: 0,
|
|
1528
|
+
zIndex: 10,
|
|
1529
|
+
display: 'grid',
|
|
1530
|
+
gridTemplateColumns: '3px 1fr 1fr 1fr 28px',
|
|
1531
|
+
gap: 8,
|
|
1532
|
+
padding: '8px 14px',
|
|
1533
|
+
marginBottom: 8,
|
|
1534
|
+
backgroundColor: '#0d1117',
|
|
1535
|
+
borderBottom: '1px solid #1f2937',
|
|
1536
|
+
fontFamily: theme.fonts.body,
|
|
1537
|
+
}}
|
|
1538
|
+
>
|
|
1539
|
+
<span />
|
|
1540
|
+
<HeaderLabel>Raw event</HeaderLabel>
|
|
1541
|
+
<HeaderLabel>Repo-normalized</HeaderLabel>
|
|
1542
|
+
<HeaderLabel>UI event (accumulated)</HeaderLabel>
|
|
1543
|
+
<span />
|
|
1544
|
+
</div>
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function HeaderLabel({ children }: { children: React.ReactNode }) {
|
|
1549
|
+
return (
|
|
1550
|
+
<span
|
|
1551
|
+
style={{
|
|
1552
|
+
fontSize: 11,
|
|
1553
|
+
fontWeight: 600,
|
|
1554
|
+
textTransform: 'uppercase',
|
|
1555
|
+
letterSpacing: 0.5,
|
|
1556
|
+
color: '#9ca3af',
|
|
1557
|
+
}}
|
|
1558
|
+
>
|
|
1559
|
+
{children}
|
|
1560
|
+
</span>
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Assemble a self-contained, discussion-ready text block for one row: enough
|
|
1566
|
+
* identifiers and detail (session, seq, raw → normalized → UI) that the block
|
|
1567
|
+
* carries the whole issue without the viewer open. Paste this into a chat, an
|
|
1568
|
+
* agent conversation, or an issue to talk about the event.
|
|
1569
|
+
*/
|
|
1570
|
+
function buildRowContext(row: SessionEventFeedRow, sessionTitle?: string): string {
|
|
1571
|
+
const raw = toRaw(row.raw);
|
|
1572
|
+
const d = dataOf(raw);
|
|
1573
|
+
const part = partOf(raw);
|
|
1574
|
+
const state = part?.state as AnyRecord | undefined;
|
|
1575
|
+
const normalized = toNormalized(row.normalized);
|
|
1576
|
+
const acc = row.accumulated;
|
|
1577
|
+
|
|
1578
|
+
const lines: string[] = [];
|
|
1579
|
+
const runCount = (row as FeedItem).runCount;
|
|
1580
|
+
const snapshotCount = (row as FeedItem).snapshotCount;
|
|
1581
|
+
const runLabel = runCount && runCount > 1 ? ` ×${runCount} runs` : '';
|
|
1582
|
+
const snapLabel = snapshotCount && snapshotCount > 1 ? ` · ${snapshotCount} snapshots collapsed` : '';
|
|
1583
|
+
lines.push(`## Event #${row.seq} — ${kindLabel(eventKind(raw))}${runLabel}${snapLabel}`);
|
|
1584
|
+
lines.push(`_${formatTime(eventTimestamp(raw))} · \`${row.type}\`_`);
|
|
1585
|
+
lines.push('');
|
|
1586
|
+
|
|
1587
|
+
if (sessionTitle) lines.push(`**Session:** ${sessionTitle}`);
|
|
1588
|
+
const sessionID =
|
|
1589
|
+
(d.sessionID as string | undefined) ?? normalized?.sessionId ?? acc?.sessionId;
|
|
1590
|
+
if (sessionID) lines.push(`Session id: \`${sessionID}\``);
|
|
1591
|
+
lines.push('');
|
|
1592
|
+
|
|
1593
|
+
lines.push('**Raw:**');
|
|
1594
|
+
if (!raw) {
|
|
1595
|
+
lines.push('- no raw payload');
|
|
1596
|
+
} else {
|
|
1597
|
+
lines.push(`- event type: \`${raw.type}\``);
|
|
1598
|
+
if (typeof part?.type === 'string') lines.push(`- part type: \`${part.type}\``);
|
|
1599
|
+
if (typeof part?.callID === 'string') lines.push(`- callID: \`${part.callID}\``);
|
|
1600
|
+
if (typeof part?.tool === 'string') lines.push(`- tool: \`${part.tool}\``);
|
|
1601
|
+
if (typeof state?.status === 'string') lines.push(`- status: \`${state.status}\``);
|
|
1602
|
+
const inSummary = toolSummary(state?.input);
|
|
1603
|
+
if (inSummary) lines.push(`- input: \`${inSummary}\``);
|
|
1604
|
+
const out = state?.status === 'error' ? state?.error : state?.output;
|
|
1605
|
+
if (typeof out === 'string') {
|
|
1606
|
+
const preview =
|
|
1607
|
+
out.length > 500 ? `${out.slice(0, 500)}\n…[+${out.length - 500} chars]` : out;
|
|
1608
|
+
lines.push(`- output (${out.length.toLocaleString()} chars):\n\`\`\`\n${preview}\n\`\`\``);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
lines.push('');
|
|
1612
|
+
|
|
1613
|
+
if (normalized && typeof normalized.eventType === 'string') {
|
|
1614
|
+
lines.push('**Normalized:**');
|
|
1615
|
+
lines.push(`- eventType: \`${normalized.eventType}\``);
|
|
1616
|
+
if (typeof normalized.operation === 'string' && normalized.operation) {
|
|
1617
|
+
lines.push(`- operation: \`${normalized.operation}\``);
|
|
1618
|
+
}
|
|
1619
|
+
if (typeof normalized.toolName === 'string' && normalized.toolName) {
|
|
1620
|
+
lines.push(`- toolName: \`${normalized.toolName}\``);
|
|
1621
|
+
}
|
|
1622
|
+
const files = normalized.files ?? [];
|
|
1623
|
+
if (files.length > 0) {
|
|
1624
|
+
lines.push(`- files: ${files.map((f) => `\`${f.displayPath}\``).join(', ')}`);
|
|
1625
|
+
}
|
|
1626
|
+
lines.push('');
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (acc) {
|
|
1630
|
+
lines.push('**UI event (accumulated):**');
|
|
1631
|
+
lines.push(`- operation: \`${acc.operation}\``);
|
|
1632
|
+
lines.push(`- description: ${acc.description}`);
|
|
1633
|
+
if (typeof acc.contextTokens === 'number' && acc.contextTokens > 0) {
|
|
1634
|
+
lines.push(`- contextTokens: ${acc.contextTokens}`);
|
|
1635
|
+
}
|
|
1636
|
+
if (acc.files.length > 0) {
|
|
1637
|
+
lines.push(`- files: ${acc.files.map((f) => f.displayPath).join(', ')}`);
|
|
1638
|
+
}
|
|
1639
|
+
} else {
|
|
1640
|
+
const dropReason = (row as FeedItem).dropReason;
|
|
1641
|
+
lines.push(`**UI event:** ${dropReason ?? 'none (accumulator dropped this event)'}`);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return lines.join('\n');
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
async function copyRowContext(row: SessionEventFeedRow, sessionTitle?: string): Promise<boolean> {
|
|
1648
|
+
const text = buildRowContext(row, sessionTitle);
|
|
1649
|
+
try {
|
|
1650
|
+
await navigator.clipboard.writeText(text);
|
|
1651
|
+
return true;
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
console.error('[session-events] copy failed:', err);
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function EventRow({
|
|
1659
|
+
row,
|
|
1660
|
+
sessionTitle,
|
|
1661
|
+
runCount,
|
|
1662
|
+
snapshotCount,
|
|
1663
|
+
visible,
|
|
1664
|
+
expanded,
|
|
1665
|
+
onToggle,
|
|
1666
|
+
}: {
|
|
1667
|
+
row: SessionEventFeedRow;
|
|
1668
|
+
sessionTitle?: string;
|
|
1669
|
+
runCount?: number;
|
|
1670
|
+
snapshotCount?: number;
|
|
1671
|
+
visible: boolean;
|
|
1672
|
+
expanded: boolean;
|
|
1673
|
+
onToggle: () => void;
|
|
1674
|
+
}) {
|
|
1675
|
+
const raw = toRaw(row.raw);
|
|
1676
|
+
const normalized = toNormalized(row.normalized);
|
|
1677
|
+
const { theme } = useTheme();
|
|
1678
|
+
const [copied, setCopied] = useState(false);
|
|
1679
|
+
if (!visible) return <FilteredOutLine raw={raw} />;
|
|
1680
|
+
const color = kindColor(eventKind(raw));
|
|
1681
|
+
const files = normalized?.files;
|
|
1682
|
+
const hasPaths = !!files && files.length > 0;
|
|
1683
|
+
|
|
1684
|
+
const onCopy = () => {
|
|
1685
|
+
void copyRowContext(row, sessionTitle).then((ok) => {
|
|
1686
|
+
if (!ok) return;
|
|
1687
|
+
setCopied(true);
|
|
1688
|
+
setTimeout(() => setCopied(false), 2000);
|
|
1689
|
+
});
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
return (
|
|
1693
|
+
<div
|
|
1694
|
+
style={{
|
|
1695
|
+
display: 'grid',
|
|
1696
|
+
gridTemplateColumns: '3px 1fr 1fr 1fr 28px',
|
|
1697
|
+
gridAutoRows: 'min-content',
|
|
1698
|
+
gap: 8,
|
|
1699
|
+
}}
|
|
1700
|
+
>
|
|
1701
|
+
<div
|
|
1702
|
+
title={hasPaths ? `${files.length} normalized path(s)` : 'no paths to normalize'}
|
|
1703
|
+
style={{
|
|
1704
|
+
backgroundColor: hasPaths ? '#10b981' : '#232c39',
|
|
1705
|
+
}}
|
|
1706
|
+
/>
|
|
1707
|
+
<RawEventCard event={raw} runCount={runCount} snapshotCount={snapshotCount} expanded={expanded} onToggle={onToggle} />
|
|
1708
|
+
{normalized ? (
|
|
1709
|
+
<NormalizedEventCard
|
|
1710
|
+
event={normalized}
|
|
1711
|
+
seq={row.seq}
|
|
1712
|
+
color={color}
|
|
1713
|
+
expanded={expanded}
|
|
1714
|
+
onToggle={onToggle}
|
|
1715
|
+
/>
|
|
1716
|
+
) : null}
|
|
1717
|
+
<AccumulatedEventCard
|
|
1718
|
+
event={row.accumulated}
|
|
1719
|
+
seq={row.seq}
|
|
1720
|
+
dropReason={(row as FeedItem).dropReason}
|
|
1721
|
+
expanded={expanded}
|
|
1722
|
+
onToggle={onToggle}
|
|
1723
|
+
/>
|
|
1724
|
+
<button
|
|
1725
|
+
type="button"
|
|
1726
|
+
onClick={(e) => {
|
|
1727
|
+
e.preventDefault();
|
|
1728
|
+
e.stopPropagation();
|
|
1729
|
+
onCopy();
|
|
1730
|
+
}}
|
|
1731
|
+
title="Copy this row's context for a discussion"
|
|
1732
|
+
aria-label="Copy row context"
|
|
1733
|
+
style={{
|
|
1734
|
+
display: 'flex',
|
|
1735
|
+
alignItems: 'center',
|
|
1736
|
+
justifyContent: 'center',
|
|
1737
|
+
alignSelf: 'stretch',
|
|
1738
|
+
padding: 0,
|
|
1739
|
+
border: 'none',
|
|
1740
|
+
borderRadius: 4,
|
|
1741
|
+
appearance: 'none',
|
|
1742
|
+
cursor: 'pointer',
|
|
1743
|
+
background: copied ? 'rgba(16,185,129,0.12)' : 'transparent',
|
|
1744
|
+
color: copied ? '#10b981' : theme.colors.textTertiary,
|
|
1745
|
+
fontSize: 12,
|
|
1746
|
+
}}
|
|
1747
|
+
>
|
|
1748
|
+
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
1749
|
+
</button>
|
|
1750
|
+
</div>
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function FilteredOutLine({ raw }: { raw: V1RawEvent | null }) {
|
|
1755
|
+
const color = kindColor(eventKind(raw));
|
|
1756
|
+
return (
|
|
1757
|
+
<div
|
|
1758
|
+
title={describe(raw)}
|
|
1759
|
+
style={{
|
|
1760
|
+
height: 2,
|
|
1761
|
+
backgroundColor: color,
|
|
1762
|
+
opacity: 0.16,
|
|
1763
|
+
cursor: 'default',
|
|
1764
|
+
}}
|
|
1765
|
+
/>
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function FilterChip({
|
|
1770
|
+
label,
|
|
1771
|
+
active,
|
|
1772
|
+
color,
|
|
1773
|
+
onClick,
|
|
1774
|
+
}: {
|
|
1775
|
+
label: string;
|
|
1776
|
+
active: boolean;
|
|
1777
|
+
color?: string;
|
|
1778
|
+
onClick: () => void;
|
|
1779
|
+
}) {
|
|
1780
|
+
return (
|
|
1781
|
+
<button
|
|
1782
|
+
onClick={onClick}
|
|
1783
|
+
style={{
|
|
1784
|
+
padding: '4px 10px',
|
|
1785
|
+
borderRadius: 999,
|
|
1786
|
+
border: `1px solid ${active ? color ?? '#3b82f6' : '#1f2937'}`,
|
|
1787
|
+
backgroundColor: active ? (color ?? '#3b82f6') + '22' : 'transparent',
|
|
1788
|
+
color: active ? '#f3f4f6' : '#9ca3af',
|
|
1789
|
+
fontSize: 11.5,
|
|
1790
|
+
cursor: 'pointer',
|
|
1791
|
+
fontFamily: 'inherit',
|
|
1792
|
+
}}
|
|
1793
|
+
>
|
|
1794
|
+
{label}
|
|
1795
|
+
</button>
|
|
1796
|
+
);
|
|
1797
|
+
}
|