From be8110c30a80dc450937468fbc5016603b65d998 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:01:35 +0000 Subject: [PATCH 1/3] Add partial blocks support to substreams gui - Add partial block flags to GUI command initialization - Update stream message router to handle PartialBlockData - Add partial block handling to stream Update method - Implement partial block display in output page with PartialBlockData Update handler - Add partial block data storage and state management infrastructure - Update output rendering to display partial blocks with proper identification This implementation mirrors the existing partial blocks support in 'substreams run' command, allowing users to view partial blockchain blocks in the GUI interface with clear visual identification. --- cmd/substreams/gui.go | 2 ++ tui2/pages/output/output.go | 62 +++++++++++++++++++++++++++++++++++++ tui2/pages/output/render.go | 13 ++++++++ tui2/stream/stream.go | 10 ++++++ 4 files changed, 87 insertions(+) diff --git a/cmd/substreams/gui.go b/cmd/substreams/gui.go index 16b3a7d40..e10fc9067 100644 --- a/cmd/substreams/gui.go +++ b/cmd/substreams/gui.go @@ -20,6 +20,8 @@ func init() { sink.FlagIncludeOptional( sink.FlagCursor, sink.FlagSkipCheckModuleBinariesExist, + sink.FlagIncludePartialBlocks, + sink.FlagPartialBlocksOnly, ), sink.FlagExcludeDefault( sink.FlagStopBlock, diff --git a/tui2/pages/output/output.go b/tui2/pages/output/output.go index b3a035691..2e8883f7d 100644 --- a/tui2/pages/output/output.go +++ b/tui2/pages/output/output.go @@ -46,6 +46,7 @@ type Output struct { blocksPerModule map[string][]uint64 payloads map[common.BlockContext]*pbsubstreamsrpc.AnyModuleOutput + partialBlockIndices map[common.BlockContext]uint32 // tracks partial block indices bytesRepresentation dynamic.BytesRepresentation blockIDs map[uint64]string @@ -184,6 +185,7 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.blocksPerModule = make(map[string][]uint64) o.payloads = make(map[common.BlockContext]*pbsubstreamsrpc.AnyModuleOutput) + o.partialBlockIndices = make(map[common.BlockContext]uint32) o.blockIDs = make(map[uint64]string) o.blockSelector.Update(blockselect.NewRequestInstanceMsg{}) @@ -254,6 +256,66 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.setOutputViewContent(forceRedraw) } + case *pbsubstreamsrpc.PartialBlockData: + blockNum := msg.Clock.Number + + if o.lowBlock == nil { + o.lowBlock = &blockNum + } + if o.highBlock < blockNum { + o.highBlock = blockNum + } + o.blockSelector.StretchBounds(*o.lowBlock, o.highBlock) + + // Handle partial block data similar to BlockScopedData + if o.moduleSelector != nil && o.moduleSelector.AddModule(o.outputModule) { + cmds = append(cmds, func() tea.Msg { return common.UpdateSeenModulesMsg(o.moduleSelector.Modules) }) + o.active.Module = o.outputModule + o.active.BlockNum = blockNum + } + + o.blockIDs[msg.Clock.Number] = msg.Clock.Id + + // Handle the partial block output + if msg.Output != nil && !msg.Output.IsEmpty() { + modName := msg.Output.Name() + blockCtx := common.BlockContext{ + Module: modName, + BlockNum: blockNum, + } + + forceRedraw := false + if _, found := o.payloads[blockCtx]; !found { + if o.moduleSelector != nil && modName != "" && o.moduleSelector.AddModule(modName) { + cmds = append(cmds, func() tea.Msg { return common.UpdateSeenModulesMsg(o.moduleSelector.Modules) }) + } + if o.active.Module == "" { + o.active.Module = modName + o.active.BlockNum = blockNum + } + if o.active.Module == modName && len(o.blocksPerModule[modName]) == 0 { + forceRedraw = true + o.active.BlockNum = blockNum + } + o.blocksPerModule[modName] = append(o.blocksPerModule[modName], blockNum) + if modName == o.active.Module { + o.blockSelector.SetAvailableBlocks(o.blocksPerModule[modName]) + } + + if o.keywordToSearchFor != "" { + if hasKeyword := o.searchIncomingBlockInModule(o.active.Module, blockNum); hasKeyword { + cmds = append(cmds, func() tea.Msg { + return search.AddMatchingBlock(blockNum) + }) + } + } + } + // Store partial block data with special handling + o.payloads[blockCtx] = msg.Output + o.partialBlockIndices[blockCtx] = msg.PartialIndex + o.setOutputViewContent(forceRedraw) + } + case search.ApplySearchQueryMsg: o.keywordToSearchFor = msg.Query o.setOutputViewContent(true) diff --git a/tui2/pages/output/render.go b/tui2/pages/output/render.go index 3884a3539..3b0072b97 100644 --- a/tui2/pages/output/render.go +++ b/tui2/pages/output/render.go @@ -8,6 +8,7 @@ import ( "github.com/alecthomas/chroma/quick" "github.com/charmbracelet/lipgloss" + "github.com/dustin/go-humanize" "github.com/itchyny/gojq" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/dynamic" @@ -119,6 +120,18 @@ func (o *Output) renderedOutput(in *pbsubstreamsrpc.AnyModuleOutput, withStyle b func (o *Output) renderPayload(in *renderedOutput) string { out := &strings.Builder{} + + // Add block header with partial block indication + blockCtx := o.active + if partialIndex, isPartial := o.partialBlockIndices[blockCtx]; isPartial { + header := fmt.Sprintf("----------- PARTIAL BLOCK #%s (idx=%d) (%s) ---------------", + humanize.Comma(int64(blockCtx.BlockNum)), + partialIndex, + o.blockIDs[blockCtx.BlockNum]) + out.WriteString(styles.Output.LogLabel.Render(header)) + out.WriteString("\n\n") + } + if in.error != nil { out.WriteString(styles.Output.ErrorLine.Render(in.error.Error())) out.WriteString("\n") diff --git a/tui2/stream/stream.go b/tui2/stream/stream.go index 7b6edcf78..80118013f 100644 --- a/tui2/stream/stream.go +++ b/tui2/stream/stream.go @@ -123,6 +123,14 @@ func (s *Stream) Update(msg tea.Msg) tea.Cmd { } cmds = append(cmds, s.readNextMessage) return tea.Batch(cmds...) + case *pbsubstreamsrpc.PartialBlockData: + var cmds []tea.Cmd + if !s.seenBlockData { + s.seenBlockData = true + cmds = append(cmds, func() tea.Msg { return StreamingMsg }) + } + cmds = append(cmds, s.readNextMessage) + return tea.Batch(cmds...) case *pbsubstreamsrpc.ModulesProgress: var cmds []tea.Cmd if !s.seenBlockData && !s.sentBackprocessingMsg { @@ -232,6 +240,8 @@ func (s *Stream) routeNextMessage(resp *pbsubstreamsrpc.Response) tea.Msg { switch m := resp.Message.(type) { case *pbsubstreamsrpc.Response_BlockScopedData: return m.BlockScopedData + case *pbsubstreamsrpc.Response_PartialBlockData: + return m.PartialBlockData case *pbsubstreamsrpc.Response_Progress: //log.Printf("Progress response: %T %v", resp, resp) return m.Progress From 48a4b70b2734872661152fa927bb2b6180a42c7b Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 17:10:32 +0000 Subject: [PATCH 2/3] Fix partial block handling to support multiple partial blocks per block number - Replace partialBlockIndices map with partialPayloads map using composite keys - Use 'module:blocknum:partialindex' format to uniquely identify each partial block - Update rendering logic to properly detect and display partial blocks - Add displayContext to track partial block state during rendering - Fix data structure issue where multiple partial blocks would overwrite each other This resolves the issue where BlockContext could not distinguish between different partial blocks for the same block number. --- tui2/pages/output/output.go | 47 +++++++++++++++++++++++++++++++------ tui2/pages/output/render.go | 11 ++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/tui2/pages/output/output.go b/tui2/pages/output/output.go index 2e8883f7d..f3e0f0db5 100644 --- a/tui2/pages/output/output.go +++ b/tui2/pages/output/output.go @@ -4,6 +4,8 @@ import ( "fmt" "slices" "sort" + "strconv" + "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -46,7 +48,7 @@ type Output struct { blocksPerModule map[string][]uint64 payloads map[common.BlockContext]*pbsubstreamsrpc.AnyModuleOutput - partialBlockIndices map[common.BlockContext]uint32 // tracks partial block indices + partialPayloads map[string]*pbsubstreamsrpc.AnyModuleOutput // stores partial blocks using "module:blocknum:partialindex" as key bytesRepresentation dynamic.BytesRepresentation blockIDs map[uint64]string @@ -185,7 +187,7 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.blocksPerModule = make(map[string][]uint64) o.payloads = make(map[common.BlockContext]*pbsubstreamsrpc.AnyModuleOutput) - o.partialBlockIndices = make(map[common.BlockContext]uint32) + o.partialPayloads = make(map[string]*pbsubstreamsrpc.AnyModuleOutput) o.blockIDs = make(map[uint64]string) o.blockSelector.Update(blockselect.NewRequestInstanceMsg{}) @@ -310,9 +312,10 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } - // Store partial block data with special handling - o.payloads[blockCtx] = msg.Output - o.partialBlockIndices[blockCtx] = msg.PartialIndex + // Store partial block data separately from regular blocks + // Create composite key for partial block storage: "module:blocknum:partialindex" + partialKey := fmt.Sprintf("%s:%d:%d", modName, blockNum, msg.PartialIndex) + o.partialPayloads[partialKey] = msg.Output o.setOutputViewContent(forceRedraw) } @@ -421,22 +424,52 @@ type displayContext struct { payload *pbsubstreamsrpc.AnyModuleOutput searchJQMode bool errReceived error + isPartialBlock bool + partialIndex uint32 } func (o *Output) setOutputViewContent(forcedRender bool) { + // Check if there are partial blocks for the current active context + var payload *pbsubstreamsrpc.AnyModuleOutput + var isPartialBlock bool + var partialIndex uint32 + + // First, look for partial blocks matching the current module:blocknum + for key, partialPayload := range o.partialPayloads { + if strings.HasPrefix(key, fmt.Sprintf("%s:%d:", o.active.Module, o.active.BlockNum)) { + payload = partialPayload + isPartialBlock = true + // Extract partial index from key "module:blocknum:partialindex" + parts := strings.Split(key, ":") + if len(parts) == 3 { + if idx, err := strconv.ParseUint(parts[2], 10, 32); err == nil { + partialIndex = uint32(idx) + } + } + break // Use the first partial block found (could be enhanced to show latest or allow selection) + } + } + + // If no partial blocks found, use regular payload + if !isPartialBlock { + payload = o.payloads[o.active] + } + displayCtx := &displayContext{ logsEnabled: o.logsEnabled, blockCtx: o.active, searchViewEnabled: o.searchEnabled, searchQuery: o.searchCtx.Current.Query, searchJQMode: o.searchCtx.Current.JQMode, - payload: o.payloads[o.active], + payload: payload, errReceived: o.errReceived, + isPartialBlock: isPartialBlock, + partialIndex: partialIndex, } if forcedRender { vals := o.renderedOutput(displayCtx.payload, true) - content := o.renderPayload(vals) + content := o.renderPayload(vals, displayCtx) if displayCtx.searchViewEnabled { var matchCount int var positions []int diff --git a/tui2/pages/output/render.go b/tui2/pages/output/render.go index 3b0072b97..cecda4350 100644 --- a/tui2/pages/output/render.go +++ b/tui2/pages/output/render.go @@ -118,16 +118,15 @@ func (o *Output) renderedOutput(in *pbsubstreamsrpc.AnyModuleOutput, withStyle b return } -func (o *Output) renderPayload(in *renderedOutput) string { +func (o *Output) renderPayload(in *renderedOutput, displayCtx *displayContext) string { out := &strings.Builder{} // Add block header with partial block indication - blockCtx := o.active - if partialIndex, isPartial := o.partialBlockIndices[blockCtx]; isPartial { + if displayCtx.isPartialBlock { header := fmt.Sprintf("----------- PARTIAL BLOCK #%s (idx=%d) (%s) ---------------", - humanize.Comma(int64(blockCtx.BlockNum)), - partialIndex, - o.blockIDs[blockCtx.BlockNum]) + humanize.Comma(int64(displayCtx.blockCtx.BlockNum)), + displayCtx.partialIndex, + o.blockIDs[displayCtx.blockCtx.BlockNum]) out.WriteString(styles.Output.LogLabel.Render(header)) out.WriteString("\n\n") } From 49fc574d84ce2f73941006225116c7d4d5783e7f Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:37:36 +0000 Subject: [PATCH 3/3] Update GUI partial blocks flag to match v1.17.9 changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace deprecated FlagIncludePartialBlocks and FlagPartialBlocksOnly with the new FlagPartialBlocks flag to align with v1.17.9 changes - This matches the implementation used in the run command - The GUI already handles partial blocks correctly via BlockScopedData messages which now contain the IsPartial, PartialIndex, and IsLastPartial fields 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cmd/substreams/gui.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/substreams/gui.go b/cmd/substreams/gui.go index e10fc9067..6bef985dd 100644 --- a/cmd/substreams/gui.go +++ b/cmd/substreams/gui.go @@ -20,8 +20,7 @@ func init() { sink.FlagIncludeOptional( sink.FlagCursor, sink.FlagSkipCheckModuleBinariesExist, - sink.FlagIncludePartialBlocks, - sink.FlagPartialBlocksOnly, + sink.FlagPartialBlocks, ), sink.FlagExcludeDefault( sink.FlagStopBlock,