Skip to content

Fix: Render an expanded subagent block's rows together under its header - #1614

Merged
huang195 merged 4 commits into
rossoctl:mainfrom
huang195:fix/subagent-block-expand
Nov 1, 2026
Merged

huang195 merged 4 commits into
rossoctl:mainfrom
huang195:fix/subagent-block-expand

Conversation

@huang195

@huang195 huang195 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

What

Two changes to the subagent blocks from #1601, from its live check on session
3230ebba ("Cortex PR 1611 Merge Conflict"):

  1. An expanded block's rows render together, under its header — pulled out
    of the chronological interleaving with the main conversation.
  2. Member exchanges are numbered from 1 within the block, while main rows
    keep session-wide numbers.

Why

Expanding ▾─ review cortex#1611 r1 worked, but the rows directly under the
header were the main conversation's — a subagent's exchange spans several
main-conversation turns (and other subagents'), so its rows scattered across
the pane at their own timestamps. That reads as "expanding did nothing". And
the # numbers the members did show were session-wide (starting at 94), which
says nothing about how much the subagent itself did — the block reads as the
subagent's own transcript, so its first message should be 1.

The merged tests' fixture never interleaved a main row inside a subagent
exchange, so the interleaved case was never pinned.

The changes

Contiguity — rebuildEventsTable's row loop: when a block's header is
emitted and the block is open, all of b.rows (already the inactive-filtered
population, in arrival order) render immediately after the header; member rows
no longer render at their own loop turn. Cell building moved into a small
renderEventRow(j) helper used by both paths so rows / visibleRows /
search rows cannot drift apart. Membership is by subagent id over the whole
session, not time-contiguity, so interleaving cannot split a block into
segments or emit a second header for the same agent — pinned by
TestBlocks_ConcurrentSubagentsEachStayOneSegment (two subagents interleaved
with each other and the main conversation, one resuming long after it started).

Numbering — a block-local blockIDs map renumbers member exchanges from 1,
built after the blocks are collected; the session-wide ids still decide what an
exchange IS (request + response share one number), only the counting restarts
per block. The header's rollup counts the same unit (28 msgs, not 56 rows)
so it agrees with the last number shown inside the block.

Unchanged: the header anchors at the block's first surviving row; main rows
keep their positions and session numbers; TIME stays absolute; the DIR
gutter's │ still marks members; sorting disables grouping (blocks exist only
in the chronological view, so the sort-key path never sees a member row);
cursor pin, search, hideInactive and the Timeline reveal all key on
visibleRows/event identity, not row order — # is display-only.

Verification

  • New tests: TestBlocks_ExpandedMembersStayTogether (a main row
    chronologically inside a subagent's exchange stays outside the expanded
    block), TestBlocks_ConcurrentSubagentsEachStayOneSegment,
    TestBlocks_MembersNumberFromOneInsideTheBlock (member # cells read
    1,1,2,2 and main rows keep 1..3; header says 2 msgs). All fail on main's
    behavior where they apply.
  • Full cmd/agentop/tui package green; gofmt/go vet clean.
  • Drove the built binary headless against the live proxy on the original
    session: Enter on ▸─ review cortex#1611 r1 · 28 msgs → ▾, the rows under
    the header are all the subagent's glm exchanges numbered 1,1,2,2,3…, no
    main-conversation rows interleaved; r2 (34 msgs) stays collapsed.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Expanded subagent blocks now display their rows together beneath the block header, even when those rows are interleaved chronologically with other events.
    • Exchange numbers within each block now start at 1, while main rows retain session-wide numbering.
    • Subagent headers now show the number of messages in the block, using “msg” for one and “msgs” for multiple.

An open block's member rows rendered at their own chronological
positions, interleaved with the main conversation's — so expanding a
header left the rows directly under it belonging to the main
conversation, which read as the expansion doing nothing (rossoctl#1047 live
check). The block's rows now render contiguously under the header, in
arrival order within the block; the TIME and # columns still carry
where each row sat, and the DIR gutter's │ still marks members.

Blocks exist only in the chronological view (a sort disables
grouping), so the sort-key path never sees a member row.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 requested a review from a team as a code owner October 30, 2026 16:41
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7643323a-1479-47dd-b1d6-4115411aa5a9
Commits

Reviewing files that changed from the base of the PR and between 5b91db4 and 2d97f8a.

Files selected for processing: 5
  • CLAUDE.md
  • cmd/agentop/tui/events_blocks.go
  • cmd/agentop/tui/events_blocks_test.go
  • cmd/agentop/tui/events_columns.go
  • cmd/agentop/tui/events_pane.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

Expanded subagent rows now appear together beneath their block headers. Their exchanges use block-local numbering, while main-conversation rows retain session-wide numbering. Headers report exchange counts as messages.

Changes: Subagent event blocks

Layer / File(s) Summary
Block-local numbering and counts
cmd/agentop/tui/events_blocks.go, cmd/agentop/tui/events_columns.go, cmd/agentop/tui/events_pane.go, cmd/agentop/tui/events_blocks_test.go
Subagent exchanges receive block-local numbers, shared by each request and response. The # column uses those numbers for block rows and session-wide numbers for main rows. Headers report message counts. Tests cover numbering and counts.
Grouped row rendering
cmd/agentop/tui/events_pane.go, cmd/agentop/tui/events_blocks_test.go, CLAUDE.md
Expanded block rows render together beneath each header; collapsed rows are omitted. Tests cover interleaved events and contiguous block rows. The documentation describes the grouping and numbering.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant events_pane
  participant cellContext
  participant column_renderers
  participant event_table
  participant search_results
  events_pane->>cellContext: Attach block-local exchange numbers
  events_pane->>column_renderers: Render grouped member rows
  column_renderers-->>events_pane: Return row cells
  events_pane->>event_table: Append header and grouped rows
  events_pane->>search_results: Append rendered rows when search is active
Loading

Suggested labels: ready-for-ai-review

Suggested reviewers: mrsabath, esnible

Merge Risk: Low · up to 2d97f

Live follow can leave the detail pane on an older main event when new subagent activity arrives. The event remains available through manual navigation, so this is mergeable with a follow-up.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: rendering expanded subagent rows together beneath their header.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 7 functions across 4 files. (1 skipped: 1 unsupported.)

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Two subagents interleaved with each other and with the main
conversation, one resuming long after it started: each block still
renders its whole run contiguously under its own single header —
membership is by subagent id over the whole session, not by
time-contiguity, so interleaving cannot split a block or emit a second
header for the same agent.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
The block reads as the subagent's own transcript, so its first
exchange is 1 — the session-wide number the main rows keep would
start a block at 94 and say nothing about how much the subagent
itself did. A request and its response still share one number (the
session-wide ids still decide what an exchange is; only the counting
restarts per block), and the header's rollup counts the same unit
(28 msgs, not 56 rows) so it agrees with the last number shown
inside the block.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The contiguity change does what it says. renderEventRow removes the duplicated cell-building path, member rows now render only from the header branch, and b.rows is the same inactive-filtered population the loop used to render, so nothing is double-counted. The sort path is unaffected because blocks exist only in the chronological view. One blocking issue: the block-local renumbering gives a request and its response different numbers when two of the same subagent's exchanges overlap. The # column exists to pair exchanges, so this breaks the invariant the PR description and the code comments both state. The fix is a few lines; details are inline.

Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (agentop TUI), Docs (CLAUDE.md)
Agent/IDE config (.claude/.vscode): none
Commits: 3 commits, all signed-off: yes
CI status: passing (all required checks green; CodeRabbit/Spellcheck skipped)
Security: no secrets, no new dependencies, no workflow/Dockerfile changes

Verified locally on 1dd7cfb (go 1.26.10): the cmd/agentop/tui package passes with -count=1, and go vet and gofmt are clean. A scratch test with overlapping exchanges in one block (below) reproduces the misnumbering, and the suggested fix produces 1,2,1,2.

Comment thread cmd/agentop/tui/events_pane.go Outdated
if !seen[ids[e]] {
seen[ids[e]] = true
}
blockIDs[e] = len(seen)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must-fix: blockIDs[e] = len(seen) assigns each row the count of distinct exchanges seen so far, not that exchange's own block-local number. It is only correct when one exchange in a block finishes before the next one starts.

When two exchanges from the same subagent overlap (which computeEventPairs and the PHASE note above explicitly allow for: A starts, B starts, A ends, B ends), you get:

req  a1.1 → 1
req  a1.2 → 2
resp a1.1 → 2   // should be 1
resp a1.2 → 2

The response of exchange 1 then shows as part of exchange 2, which is exactly the mis-pairing the # column is there to prevent. It also contradicts "a request and its response share one number there, and therefore here". I reproduced this with a scratch test (req a1.1, req a1.2, resp a1.1, resp a1.2, block expanded): the rows read 1,2,2,2.

Map each session id to the block-local number assigned the first time it is seen:

local := make(map[int]int, len(b.rows))
for _, j := range b.rows {
	e := ctxs[j].row.event
	n, ok := local[ids[e]]
	if !ok {
		n = len(local) + 1
		local[ids[e]] = n
	}
	blockIDs[e] = n
}
b.msgs = len(local)

With this change the same scratch test reads 1,2,1,2, and the full package stays green. (It also removes the if !seen[...] { seen[...] = true } guard, which was a no-op.)

}
events := []pipeline.SessionEvent{
req("m1", ""), resp("m1", ""), // session exchange 1
req("a1.1", "a1"), resp("a1.1", "a1"), // session 2 — block-local 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: this fixture's in-block exchanges are strictly sequential (req, resp, req, resp), so it cannot catch the overlap case in events_pane.go. Please add an overlapping pair, e.g. req(a1.1), req(a1.2), resp(a1.1), resp(a1.2), and assert that the expanded # cells read 1,2,1,2. That pins the pairing invariant for block-local numbers the same way TestBlocks_ConcurrentSubagentsEachStayOneSegment pins contiguity.

Fixes review: overlapping exchanges in one subagent block got the count of exchanges seen so far, so a response could show the number of the next exchange
Files:
- cmd/agentop/tui/events_pane.go
- cmd/agentop/tui/events_blocks_test.go

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of 2d97f8a. Both items from the previous review are resolved. events_pane.go now maps each session id to a block-local number assigned on first sight, so a request and its response share a number even when exchanges overlap. TestBlocks_OverlappingExchangesKeepTheirNumbers pins 1,2,1,2 and the 2 msgs rollup. I checked that the new test fails on 1dd7cfb's logic ([1 2 2 2]) and passes on the fix. The incremental diff touches nothing else, and I have no new findings.

Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (agentop TUI), Docs (CLAUDE.md)
Agent/IDE config (.claude/.vscode): none
Commits: 4 commits, all signed-off: yes
CI status: passing (all checks green; Spellcheck skipped)

Verified locally on 2d97f8a (go 1.27.3): go test -count=1 ./cmd/agentop/tui/... passes, and go vet and gofmt -l are clean.

@huang195
huang195 merged commit d7a1585 into rossoctl:main Nov 1, 2026
41 checks passed
@huang195
huang195 deleted the fix/subagent-block-expand branch November 1, 2026 18:55
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants