Skip to content

Feat(abctl): one row per network message, show every message#527

Merged
huang195 merged 7 commits into
rossoctl:mainfrom
huang195:feat/abctl-one-row-per-message
Jun 22, 2026
Merged

Feat(abctl): one row per network message, show every message#527
huang195 merged 7 commits into
rossoctl:mainfrom
huang195:feat/abctl-one-row-per-message

Conversation

@huang195

@huang195 huang195 commented Jun 19, 2026

Copy link
Copy Markdown
Member

Problem

The abctl session timeline was confusing for two reasons:

  1. Messages no plugin processed were invisible. Every listener append site was gated by if ev.MCP != nil || ev.Inference != nil || ev.Invocations != nil || plugins != nil — so a request/response no plugin acted on (a generic example.com 404, a passthrough call) was never stored, and abctl couldn't show it.
  2. One message showed N times. The events pane emitted one row per plugin invocation (cartesian SessionEvent × Invocation), so a single message touched by N plugins appeared as N rows. A TLS-bridged call also showed twice (the host:443 CONNECT event + the decrypted inner-request event).

Goal: a clean one-row-per-network-message timeline that shows every message, with per-plugin detail on drill-in.

Changes

Server — record every message (authbridge/authlib)

  • Relaxed the four-clause append gate to unconditional at every accept-path emit site: forward-proxy request/response, transparent tunnel-open, reverse-proxy request/response/streaming. The outer !skipped guards are untouched, so listener.skip_hosts traffic stays suppressed by design.
  • Raised the session.max_events default 100 → 500 in all three binaries (proxy/envoy/lite) to absorb the ~2× event volume; still tunable.

abctl — one row per message (authbridge/cmd/abctl/tui)

  • One table.Row per SessionEvent. eventAction() folds an event's per-plugin invocations into one ACTION + PLUGIN cell, headlining the highest-ranked enforced action — deny > modify > observe > allow > skip.
    • observe outranks allow so a parser (which supplies the METHOD shown on the row) headlines over a gate that merely allowed — keeping PLUGIN consistent with METHOD.
    • A skip never credits a plugin: a skip-only or no-plugin message shows passthrough markers (— —), not the name of a plugin that declined to act.
    • A shadow deny/modify never headlines over the enforced action (the pipeline enforces deny only when !Shadow); it's surfaced with a trailing * (e.g. allow*), or headlines alone (deny*) when nothing enforced acted.
  • Detail pane shows the whole event (all invocations, both directions); a bridged row also folds the CONNECT tunnel's gate invocations into its ACTION + inactive-filter view.
  • CONNECT fold. A TLS-bridge CONNECT tunnel-open folds into the decrypted inner request that follows it — a bridged call is one request row + one response row, with a tunnel: summary in the detail pane. Two back-to-back passthrough CONNECTs to the same host are not folded.
  • Connector glyphs. Event-level span glyphs (//) in PHASE bracket each request/response exchange and nest outbound calls under the inbound request that caused them. A pair nested 3+ deep keeps its own / corners (the inner glyph tracks the row's narrowest containing span, not the second-widest), so deeply-nested exchanges still read as connected.
  • s toggle hides passthrough/skip-only messages — default off = show everything.
  • Replaces the per-invocation row model (drops flattenInvocations, per-invocation pairing, eventScopedToPlugin; span rendering now operates on events).

abctl — page navigation

  • With 500 messages per session, bound page-up/page-down to page the active pane by a near-full screen (one row of overlap, clamped to range); the detail viewport delegates to its built-in page scroll. Bound to b/f (less/vim style, work on any keyboard — Mac laptops have no dedicated Page keys) as well as PgUp/PgDn. Footer shows [b/f] page.

Also included — fix MCP/SSE responses not being parsed

While e2e-testing the timeline, MCP tool calls (tools/list, tools/call) showed the request parsed (observe / mcp-parser) but the response blank — no result, no invocation. Root cause: plugins.Build wraps every Configurable plugin in pipeline.configuredPlugin (to surface raw config on /v1/pipeline), and that wrapper embeds the Plugin interface — Go doesn't promote method-set membership through an embedded interface, so the wrapped plugin's OnResponseFrame is lost and it fails the StreamingResponder assertion in RunResponseFrame/HasStreamingResponders. mcp-parser has Configure (so it's wrapped and broke); inference-parser doesn't (so it worked).

  • Fix: WrapConfigured returns a StreamingResponder-preserving wrapper only when the inner plugin implements it, forwarding OnResponseFrame. A distinct type (not an unconditional no-op) keeps HasStreamingResponders exact, so the streaming-vs-buffered path selection is unchanged for non-streaming pipelines.
  • Hardening: mcp-parser's OnResponseFrame now parses via the SSE-aware parseMCPResponse (not a bare json.Unmarshal), so the buffered whole-body dispatch handling a raw data: {...} SSE blob records the result instead of dropping it.
  • Verified e2e on Kind: the tools/list response now records result + mcp-parser observe.

Tests

  • Server: empty-pipeline request/response is recorded as two events; WrapConfigured preserves StreamingResponder for a Configurable+streaming plugin and does not promote a non-streaming one; mcp-parser parses a raw-SSE-blob frame; forward-proxy integration drives a tools/list SSE response through a WrapConfigured'd mcp-parser and asserts it's parsed.
  • abctl: eventAction precedence (incl. observe > allow, shadow → *, skip → passthrough); eventInactive; multi-plugin → 1 row; empty-invocations response → 1 row with status; CONNECT+inner → 1 collapsed row; passthrough/two-CONNECT not folded; tunnel invocations folded into the row; event-level pairing + nested + triple-nested span glyphs; hideInactive integration; page-up/down (PgUp/PgDn + b/f).

go test ./... green for both modules; go vet + gofmt clean; all three release binaries build under GOWORK=off.

Notes / follow-ups

  • Scoped to the proxy-sidecar listeners (forward/reverse/transparent), where the TLS bridge lives. The envoy-sidecar ext_proc listener recording is unchanged in this PR.
  • CONNECT↔inner folding is client-side adjacency matching (wire format unchanged). If concurrency makes adjacency flaky, a follow-up can add a server-side correlation id on the inner event.

Assisted-By: Claude Code

Summary by CodeRabbit

  • New Features

    • Added "hide inactive messages" toggle in event viewer to filter passthrough/skip-only traffic.
    • Improved events display to show one row per network message instead of per invocation.
    • Enhanced tunnel/CONNECT event visibility in detail view with bridging information.
  • Bug Fixes

    • Session recording now captures all messages, including those with no plugin activity.
    • Fixed streaming response handling for SSE-formatted responses.
  • Configuration

    • Increased default session event limit from 100 to 500.

@huang195
huang195 requested a review from a team as a code owner June 19, 2026 18:21
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@huang195, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 36 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f1f880d-b4a0-492b-95b4-c93903e4da24

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb3eb1 and 58ff6d8.

📒 Files selected for processing (1)
  • authbridge/authlib/listener/reverseproxy/finisher_test.go
📝 Walkthrough

Walkthrough

This PR removes plugin-activity gating from session event recording across forward proxy, reverse proxy, and transparent proxy, so every network message is recorded regardless of plugin participation. It adds an explicit Tunnel boolean flag to SessionEvent for opaque tunnel-open marking. It fixes MCP SSE response frame decoding to handle raw data:-prefixed SSE blobs, preserves StreamingResponder through WrapConfigured, raises the default maxEvents from 100 to 500, and refactors the TUI from a per-invocation flat row model to a per-message event-row model with CONNECT tunnel folding.

Changes

Unconditional Session Recording and Downstream Fixes

Layer / File(s) Summary
maxEvents default raised to 500
authbridge/authlib/config/config.go, authbridge/cmd/authbridge-envoy/main.go, authbridge/cmd/authbridge-lite/main.go, authbridge/cmd/authbridge-proxy/main.go
Config field comment and all three binary entry points raise the default session maxEvents from 100 to 500.
Tunnel flag added to SessionEvent
authbridge/authlib/pipeline/session.go, authbridge/authlib/pipeline/session_test.go
SessionEvent gains an explicit Tunnel boolean field with JSON wire representation (marshal/unmarshal); test coverage expanded to include round-trip serialization.
Transparent proxy tunnel recording with Tunnel marker
authbridge/authlib/listener/forwardproxy/transparent.go, authbridge/authlib/listener/forwardproxy/transparent_test.go
recordTunnelOpened now always appends SessionEvent with explicit Tunnel: true marker; conditional gate on plugin snapshots is removed; test validates tunnel-open events are recorded with flag set.
WrapConfigured preserves StreamingResponder
authbridge/authlib/pipeline/configured.go, authbridge/authlib/pipeline/configured_test.go
New configuredStreamingPlugin wrapper type preserves streaming-responder behavior for plugins that implement StreamingResponder; WrapConfigured selects wrapper type accordingly; two regression tests verify streaming preservation and non-promotion.
MCP SSE response frame parsing and integration tests
authbridge/authlib/plugins/mcpparser/plugin.go, authbridge/authlib/plugins/mcpparser/streaming_test.go, authbridge/authlib/listener/forwardproxy/mcp_sse_repro_test.go
OnResponseFrame switches to parseMCPResponse for SSE-aware decoding of raw data: blobs; unit test covers raw SSE blob parsing; end-to-end forward-proxy test pins the MCP SSE observe flow with streaming responder.
Forward proxy unconditional session recording
authbridge/authlib/listener/forwardproxy/server.go, authbridge/authlib/listener/forwardproxy/server_test.go
serveOutbound and recordOutboundResponseEvent remove the plugin-activity gate and unconditionally append SessionEvent records; test validates recording occurs even with empty outbound pipeline.
Reverse proxy unconditional session recording
authbridge/authlib/listener/reverseproxy/server.go
handleRequest, modifyResponse (buffered path), and recordInboundResponseEvent (streaming path) remove the A2A/invocations/plugins gate and unconditionally append SessionEvent records when sessions are enabled.

TUI Event-Row Model Refactor

Layer / File(s) Summary
Model state refactored for event-row display
authbridge/cmd/abctl/tui/app.go, authbridge/cmd/abctl/tui/events_pane.go
model struct replaces showSkips/hiddenSkips/detailInvocation/invocationRow-based visibleRows with hideInactive/hiddenInactive/detailRow/eventRow-based visibleRows; eventRow type with optional folded tunnel pointer and invocations() aggregator introduced.
Event-row table building, pairing, filtering, and rendering
authbridge/cmd/abctl/tui/events_pane.go
rebuildEventsTable, buildEventRows, computeEventPairs, computeSpanGlyphs, matchEventRow, eventAction, and eventInactive are rewritten or added; tunnel folding predicates, host parsing, and invocation aggregation implemented; net import and PHASE column width adjusted.
Event-row table tests
authbridge/cmd/abctl/tui/events_pane_test.go
Test suite refactored to event-level logic: validates ACTION/PLUGIN aggregation precedence, event "inactive" semantics, CONNECT bridging and tunnel folding, hideInactive integration, request/response pairing with method discrimination, and filter matching shortcuts; PHASE glyph tests updated.
Detail pane refactored to accept eventRow
authbridge/cmd/abctl/tui/detail_pane.go, authbridge/cmd/abctl/tui/detail_pane_test.go
showDetail rewritten to accept eventRow; tunnelHeader helper added for CONNECT tunnel summary; eventScopedToPlugin removed; tests replaced with tunnelHeader coverage.
Key bindings and layout wired to event-row model
authbridge/cmd/abctl/tui/keys.go
s keybinding toggles hideInactive; enter/detail navigation uses selectedEventRow() + showDetail(er); pagination delegated to pageActivePane helper; resize re-render uses showDetail(m.detailRow); help footer updated with hiddenInactive count.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • kagenti/kagenti-extensions#480: Directly modifies the forward-proxy SSE response handling path and MCP OnResponseFrame dispatch, which this PR's SSE blob parsing fix and session recording changes build on.
  • kagenti/kagenti-extensions#485: Introduced recordTunnelOpened and tunnel-open session event appending in the transparent proxy, which this PR makes unconditional and extends with the Tunnel flag.
  • kagenti/kagenti-extensions#470: Modifies showDetail signature and plugin-scoping behavior in detail_pane.go, the same function this PR rewrites to accept eventRow.

Suggested reviewers

  • cwiklik
  • pdettori

Poem

🐇 Every hop gets logged now, no message left behind,
Five hundred events stored, of the passthrough kind.
SSE frames decoded, the tunnel folded neat,
The TUI rows reborn, per-message — oh, how sweet!
No plugin? Still recorded! The rabbit hops with glee. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat(abctl): one row per network message, show every message' directly and clearly summarizes the main changes: restructuring the timeline from per-invocation rows to per-message rows, and ensuring all messages (including those with no plugin activity) are displayed.
Docstring Coverage ✅ Passed Docstring coverage is 84.13% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

huang195 added 2 commits June 19, 2026 15:23
The session timeline was confusing for two reasons: messages no plugin
processed were invisible, and a message processed/skipped by N plugins
showed as N rows (plus a separate row for a TLS-bridged call's CONNECT).

Server (authlib): relax the four-clause append gate to unconditional at
every accept-path emit site (forward-proxy request/response, transparent
tunnel-open, reverse-proxy request/response/streaming) so every message
the pipeline saw is recorded — including passthrough requests and generic
responses (e.g. a 404) no plugin touched. The !skipped guards stay, so
listener.skip_hosts traffic remains suppressed. Raise session.max_events
default 100 -> 500 in all three binaries to absorb the ~2x volume.

abctl: render one table.Row per SessionEvent. eventAction() folds a
message's per-plugin invocations into one ACTION + PLUGIN cell, headlining
the highest-ranked ENFORCED action — deny > modify > observe > allow >
skip. observe outranks allow so a parser (which supplies METHOD) headlines
over a gate that merely allowed; a skip-only or no-plugin message shows
passthrough markers ("—") rather than crediting a plugin that declined to
act. A shadow deny/modify never headlines over the action that really took
effect (the pipeline enforces deny only when !Shadow); it is surfaced with
a trailing "*" instead (e.g. "allow*"), or headlines alone ("deny*") when
nothing enforced acted. The detail pane shows the whole event; a bridged
row also folds the CONNECT tunnel's gate invocations into its ACTION and
inactive-filter view.

A TLS-bridge CONNECT tunnel folds into the decrypted inner request that
follows it, so a bridged call is one request row + one response row, with
a "tunnel:" summary in the detail pane. Two back-to-back passthrough
CONNECTs to the same host are NOT folded (each is its own message).
Event-level span glyphs in PHASE bracket each request/response exchange
and nest outbound calls under the inbound request that caused them. The
`s` key hides passthrough/skip-only messages (default off = show all).

Replaces the per-invocation row model: drops flattenInvocations and the
per-invocation pairing; the span-glyph rendering now operates on events,
so a multi-plugin message no longer duplicates into one row per plugin.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
MCP tool responses (tools/list, tools/call) over the Streamable HTTP
transport are text/event-stream, dispatched to plugins via
StreamingResponder.OnResponseFrame. In abctl these calls showed the
request parsed (observe) but the response blank (no result, no
invocation) — the response was never parsed.

Root cause: plugins.Build wraps every Configurable plugin in
pipeline.configuredPlugin (to surface raw config on /v1/pipeline). That
wrapper embeds the Plugin INTERFACE, and Go does not promote method-set
membership through an embedded interface — so the wrapped plugin's
OnResponseFrame is not promoted, and the wrapper fails the
StreamingResponder type-assertion in Pipeline.RunResponseFrame /
HasStreamingResponders. mcp-parser implements Configure (Configurable),
so it gets wrapped and silently loses response-frame dispatch;
inference-parser has no Configure, isn't wrapped, and worked. The
listener then takes a path that records the response event with no
parser activity, leaving result/invocations empty.

Fix: WrapConfigured returns a StreamingResponder-preserving wrapper
(configuredStreamingPlugin) when, and only when, the inner plugin
implements StreamingResponder — forwarding OnResponseFrame to it. Using a
distinct type (rather than an unconditional no-op OnResponseFrame on
configuredPlugin) keeps HasStreamingResponders exact, so the listener's
streaming-vs-buffered path selection is unchanged for non-streaming
pipelines.

Also harden mcp-parser's OnResponseFrame to parse via the SSE-aware
parseMCPResponse instead of a bare json.Unmarshal, so the buffered
whole-body dispatch (which can hand it a raw "data: {...}" SSE blob)
records the result instead of silently dropping it.

Tests: pipeline regression (a Configurable+StreamingResponder plugin
stays a StreamingResponder after wrapping; a non-streaming Configurable
plugin does not become one); mcp-parser raw-SSE-blob frame records an
observe; forward-proxy integration test drives a tools/list SSE response
through the proxy with a WrapConfigured'd mcp-parser and asserts the
response is parsed + observed. Verified e2e on Kind: the tools/list
response now records result + mcp-parser observe.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 force-pushed the feat/abctl-one-row-per-message branch from e69d3c7 to 6cc58fe Compare June 19, 2026 20:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/cmd/abctl/tui/events_pane.go`:
- Around line 212-241: The isTunnelOpen function currently infers CONNECT
tunnels using heuristics (checking Direction, Phase, protocol extension absence,
and hasPort), which can incorrectly match ordinary unparsed HTTP messages and
cause them to be misfolded with subsequent same-host requests. Replace the
heuristic checks in isTunnelOpen with an explicit tunnel-open marker from the
SessionEvent producer, such as a dedicated boolean field or enum value on the
SessionEvent that explicitly indicates tunnel-open status, to ensure accurate
detection and prevent unrelated events from being incorrectly folded in
isBridgedInner.
- Around line 506-507: The eventMethod function truncates method names to 22
characters for display purposes, but it is being used directly in comparison
logic at lines 506-507 which causes incorrect pairing of methods with identical
prefixes and breaks filtering on suffixes. Replace the eventMethod(*ri) and
eventMethod(*rj) comparisons with direct access to the raw untruncated method
values from the request objects (such as ri.Method and rj.Method). Keep
eventMethod calls only for rendering/display purposes, not for business logic.
Apply the same fix to the comparison logic at lines 690-692.

In `@authbridge/cmd/abctl/tui/keys.go`:
- Around line 413-417: The help text for the `s` key toggle in the skipHint
variable does not accurately describe what the hideInactive toggle actually
controls. Update the skipHint string on line 413 from "[s] hide skips" to use
terminology that reflects all the types of inactive messages being hidden,
including skips, passthrough, and no-plugin messages, so users understand the
complete effect of toggling this option.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b36156b-8213-4459-a151-9e0464783e9f

📥 Commits

Reviewing files that changed from the base of the PR and between c52044e and 6cc58fe.

📒 Files selected for processing (19)
  • authbridge/authlib/config/config.go
  • authbridge/authlib/listener/forwardproxy/mcp_sse_repro_test.go
  • authbridge/authlib/listener/forwardproxy/server.go
  • authbridge/authlib/listener/forwardproxy/server_test.go
  • authbridge/authlib/listener/forwardproxy/transparent.go
  • authbridge/authlib/listener/reverseproxy/server.go
  • authbridge/authlib/pipeline/configured.go
  • authbridge/authlib/pipeline/configured_test.go
  • authbridge/authlib/plugins/mcpparser/plugin.go
  • authbridge/authlib/plugins/mcpparser/streaming_test.go
  • authbridge/cmd/abctl/tui/app.go
  • authbridge/cmd/abctl/tui/detail_pane.go
  • authbridge/cmd/abctl/tui/detail_pane_test.go
  • authbridge/cmd/abctl/tui/events_pane.go
  • authbridge/cmd/abctl/tui/events_pane_test.go
  • authbridge/cmd/abctl/tui/keys.go
  • authbridge/cmd/authbridge-envoy/main.go
  • authbridge/cmd/authbridge-lite/main.go
  • authbridge/cmd/authbridge-proxy/main.go

Comment thread authbridge/cmd/abctl/tui/events_pane.go
Comment thread authbridge/cmd/abctl/tui/events_pane.go Outdated
Comment thread authbridge/cmd/abctl/tui/keys.go Outdated
Comment on lines +413 to +417
skipHint := "[s] hide skips"
if m.hideInactive {
skipHint = "[s] show all"
}
base := "[↑↓] nav [↵] detail [esc] back [/] filter " + skipHint + " [p] pause [q] quit"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the s help text match hideInactive.

Line 413 says “hide skips”, but the toggle also hides passthrough/no-plugin messages. That can make hidden network messages look unexpected.

Proposed wording update
-		skipHint := "[s] hide skips"
+		inactiveHint := "[s] hide passthru/skips"
 		if m.hideInactive {
-			skipHint = "[s] show all"
+			inactiveHint = "[s] show all"
 		}
-		base := "[↑↓] nav  [↵] detail  [esc] back  [/] filter  " + skipHint + "  [p] pause  [q] quit"
+		base := "[↑↓] nav  [↵] detail  [esc] back  [/] filter  " + inactiveHint + "  [p] pause  [q] quit"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
skipHint := "[s] hide skips"
if m.hideInactive {
skipHint = "[s] show all"
}
base := "[↑↓] nav [↵] detail [esc] back [/] filter " + skipHint + " [p] pause [q] quit"
inactiveHint := "[s] hide passthru/skips"
if m.hideInactive {
inactiveHint = "[s] show all"
}
base := "[↑↓] nav [↵] detail [esc] back [/] filter " + inactiveHint + " [p] pause [q] quit"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/cmd/abctl/tui/keys.go` around lines 413 - 417, The help text for
the `s` key toggle in the skipHint variable does not accurately describe what
the hideInactive toggle actually controls. Update the skipHint string on line
413 from "[s] hide skips" to use terminology that reflects all the types of
inactive messages being hidden, including skips, passthrough, and no-plugin
messages, so users understand the complete effect of toggling this option.

huang195 added 5 commits June 19, 2026 17:10
A request/response pair nested three or more levels deep — e.g. a
tools/list pair inside an a2a message/stream span inside a long-lived
$transport/stream span — rendered "││" on both rows instead of "│┌" /
"│└", so the pair didn't read as connected. computeSpanGlyphs caps the
PHASE prefix at two levels and was picking the two WIDEST enclosing
spans, whose middle bars masked the row's own corner.

Pick the inner glyph from the row's NARROWEST containing span (its own
tightest exchange) instead of the second-widest, so an endpoint of a
deeply-nested pair always shows its ┌/└ corner. The outer glyph still
shows the broadest enclosing span for context. Adds a triple-nested
test case covering exactly this shape.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
session.max_events is now 500, so one-row-at-a-time nav through a busy
session is tedious. Bind PgUp/PgDn (and pgdn) to page the active pane by a
near-full screen — events/sessions/pipeline/catalog tables move the cursor
by (visible height − 1), one row of overlap for context, clamped to the
row range; the detail viewport delegates to its built-in page scroll.
Surface "[⇞⇟] page" in the events footer.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Mac laptops have no dedicated Page Up/Down keys (they're fn+↑/fn+↓), so
route the less/vim-style b (page up) and f (page down) through the same
pager as PgUp/PgDn — uniform one-row overlap on every keyboard — and show
"[b/f] page" in the events footer instead of the ⇞⇟ glyphs, which aren't
obvious on a laptop. Extends the page-nav test to cover b/f.

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

Three review findings on PR rossoctl#527:

- (Major) Don't infer CONNECT tunnels from host/extension shape. With
  unconditional recording, an ordinary unparsed outbound request can look
  like a tunnel-open (no protocol ext + host:port) and get wrongly folded
  with a following same-host request. Add an explicit Tunnel marker to
  SessionEvent, set by recordTunnelOpened, and key abctl's isTunnelOpen on
  it instead of the heuristic. (Drops the now-unused hasPort helper.)

- (Minor) eventMethod truncates to 22 chars for display but was used for
  pairing and search — long names sharing a 22-char prefix mis-paired, and
  truncated suffixes weren't searchable. Add eventMethodValue (raw) for
  logic; keep eventMethod (truncated) for rendering only.

- (Minor) The `s` footer hint said "hide skips" but the toggle also hides
  passthrough/no-plugin messages — now "[s] hide passthru/skip".

Tests: SessionEvent JSON round-trip covers Tunnel; recordTunnelOpened sets
the marker; abctl tunnel tests set Tunnel explicitly.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
TestReverseProxy_Finisher_{Allow,Deny} read finisherStub.seen immediately
after http.Get returned. The reverse proxy sets FlushInterval=-1, so the
streamed response reaches the client before the handler's deferred
RunFinish (which dispatches OnFinish) executes — a pre-existing race that
flaked CI under -race (~1 in 20 locally).

Poll for the expected finishers with a short timeout (waitSeen) instead of
reading once, and store finisherStub.seen LAST in OnFinish (after outcome)
so observing seen==true guarantees outcome is visible. The negative check
(after-deny must NOT fire) runs after the positive waits, by which point
the single RunFinish dispatch is complete.

Verified: 50× -race runs of the finisher tests and the full authlib -race
suite are green.

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

@cwiklik cwiklik 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.

Clean, well-structured PR that fixes a real UX problem (N rows per message → 1 row per message) and a correctness bug (WrapConfigured losing StreamingResponder).

  • Explicit Tunnel marker replaces heuristic-based CONNECT detection — eliminates a class of false-fold bugs.
  • eventAction precedence logic correctly separates enforced vs shadow actions with thorough test coverage.
  • WrapConfigured fix uses distinct types to preserve HasStreamingResponders semantics without unconditional no-ops.
  • Finisher test race properly fixed with atomic store ordering + poll-wait.
  • CONNECT→inner adjacency limitation documented with clear follow-up path.

Areas reviewed: Go (library/server), Go (CLI/TUI) — 22 files, ~2500 lines changed
Commits: 7/7 signed-off ✓
CI: 17/17 passing ✓

@huang195
huang195 merged commit f8ecb32 into rossoctl:main Jun 22, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Jun 22, 2026
@huang195
huang195 deleted the feat/abctl-one-row-per-message branch June 22, 2026 14:35
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.

3 participants