feat(leios-tui): add terminal dashboard for the Leios testnet#791
Conversation
📝 WalkthroughWalkthroughThis PR adds a new example crate ChangesLeios TUI example
Estimated code review effort: 3 (Moderate) | ~30 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
examples/leios-tui/src/logbuf.rs (1)
45-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding unit tests for
LogLayer/uptimeformatting.Unlike
dashboard.rs, this file has no tests covering line formatting, ring-buffer trimming atLOG_CAP, or the message/field extraction logic. A lightweight test using a manually constructedtracing::Subscriber(or callingMsgVisitor/uptimedirectly) would guard against future formatting regressions.🤖 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 `@examples/leios-tui/src/logbuf.rs` around lines 45 - 66, Add unit tests for LogLayer and uptime formatting to cover the current line construction in LogLayer::on_event. Create a lightweight test that exercises MsgVisitor plus uptime (or a minimal tracing::Subscriber/Event path) and verifies the formatted output includes the expected timestamp/level/message/fields. Also add a test that pushes more than LOG_CAP entries through LogLayer’s buffer path and asserts the ring buffer trims oldest entries correctly.examples/leios-tui/src/main.rs (1)
167-170: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo panic hook to restore terminal state.
ratatui::restore()at Line 169 only runs on normal return fromnode.run(). A panic insiderun()(e.g., inhandle_event, dashboard state, orui::draw) will skip the restore, leaving the terminal in raw/alternate-screen mode after the process exits.🔧 Proposed fix
let mut terminal = ratatui::init(); + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + ratatui::restore(); + default_hook(info); + })); let result = node.run(&mut terminal).await; ratatui::restore(); result🤖 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 `@examples/leios-tui/src/main.rs` around lines 167 - 170, The terminal cleanup in main only happens after node.run(&mut terminal).await returns normally, so a panic in node.run, handle_event, or ui::draw can skip ratatui::restore and leave the terminal broken. Update the startup/shutdown flow around ratatui::init and node.run to guarantee restoration on both normal exit and panic, such as by installing a panic hook or using a guard that always calls ratatui::restore before the process unwinds.
🤖 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 `@examples/leios-tui/src/dashboard.rs`:
- Around line 218-225: The `EbFetched::Block` handling in `dashboard.rs` only
updates `tx_total` and advances the row, so if `BlockTxsOffer` arrived first the
offer is never acted on. Update the block-body path to preserve offers via
`upsert_eb` and, when `body.raw_bytes()` reveals the tx count, immediately
trigger the first tx fetch window for that existing row; also make sure the same
behavior is applied in the other body-handling block referenced by the review.
- Around line 278-282: The BlockOffer handling in dashboard.rs always queues
Action::FetchEb, which causes duplicate body requests for the same EB even when
a fetch is already in flight or the body is already present. Update the
leiosnotify::Notification::BlockOffer branch in dashboard::Dashboard logic to
check the EB state before pushing FetchEb, using the existing upsert_eb/EB
tracking fields to detect whether this is the first body offer. Only emit the
fetch action when the EB has not yet had a body request or stored body, while
keeping the offered counter and logging unchanged.
In `@examples/leios-tui/src/main.rs`:
- Around line 113-135: The input handling in the main event loop is ignoring
`Some(Err(_))` from `EventStream::next`, so failures are silently dropped.
Update the `select!` branch in `main` to explicitly handle the
`self.input.next()` result in `leios-tui::main` by matching
`Option<Result<Event, Error>>`, returning an error or otherwise surfacing the
failure when the stream yields `Err`, while keeping the existing
`dashboard.handle_input` path for valid events.
In `@examples/leios-tui/src/ui.rs`:
- Around line 186-192: The selection logic in ui.rs is using a positional index
for d.selected against d.ebs.values().rev(), so adding new EB rows can move the
highlight to the wrong item when follow is off. Update the state handling around
the table_state.select block to track the selected EbId in the UI state, or
recompute d.selected when the EB list changes so the same block remains selected
after inserts. Keep the fix centered on the selection update path that uses
d.ebs, d.follow, and table_state.select.
---
Nitpick comments:
In `@examples/leios-tui/src/logbuf.rs`:
- Around line 45-66: Add unit tests for LogLayer and uptime formatting to cover
the current line construction in LogLayer::on_event. Create a lightweight test
that exercises MsgVisitor plus uptime (or a minimal tracing::Subscriber/Event
path) and verifies the formatted output includes the expected
timestamp/level/message/fields. Also add a test that pushes more than LOG_CAP
entries through LogLayer’s buffer path and asserts the ring buffer trims oldest
entries correctly.
In `@examples/leios-tui/src/main.rs`:
- Around line 167-170: The terminal cleanup in main only happens after
node.run(&mut terminal).await returns normally, so a panic in node.run,
handle_event, or ui::draw can skip ratatui::restore and leave the terminal
broken. Update the startup/shutdown flow around ratatui::init and node.run to
guarantee restoration on both normal exit and panic, such as by installing a
panic hook or using a guard that always calls ratatui::restore before the
process unwinds.
🪄 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
Run ID: 033ef479-9177-404e-adc0-c6742a02d716
📒 Files selected for processing (6)
Cargo.tomlexamples/leios-tui/Cargo.tomlexamples/leios-tui/src/dashboard.rsexamples/leios-tui/src/logbuf.rsexamples/leios-tui/src/main.rsexamples/leios-tui/src/ui.rs
| leiosfetch::Response::Block(body) => { | ||
| let n = eb_tx_count(body.raw_bytes()); | ||
| self.overlay.bodies += 1; | ||
| self.overlay.bytes += body.raw_bytes().len() as u64; | ||
| if let Some(row) = self.ebs.get_mut(eb) { | ||
| row.tx_total = Some(n); | ||
| row.advance(EbStage::BodyFetched); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fetch txs after the body arrives when the tx offer came first.
If BlockTxsOffer arrives before EbFetched::Block, the row reaches TxsOffered, but the body handler only records tx_total and returns no action, so transactions are never fetched. Preserve tx offers with upsert_eb, then trigger the first tx window when the body later provides the count.
Proposed fix
leiosfetch::Response::Block(body) => {
let n = eb_tx_count(body.raw_bytes());
+ let mut txs_fetch = None;
self.overlay.bodies += 1;
self.overlay.bytes += body.raw_bytes().len() as u64;
if let Some(row) = self.ebs.get_mut(eb) {
+ let txs_were_offered = row.stage == EbStage::TxsOffered;
row.tx_total = Some(n);
row.advance(EbStage::BodyFetched);
+ if txs_were_offered && n > 0 {
+ txs_fetch = Some(Bitmaps::all(n.min(MAX_TXS_PER_FETCH)));
+ }
+ }
+ if let Some(bitmaps) = txs_fetch {
+ actions.push(Action::FetchEbTxs(pid.clone(), eb.clone(), bitmaps));
}
tracing::info!(eb = %fmt_eb(eb), bytes = body.raw_bytes().len(), txs = n, "eb body fetched");
} leiosnotify::Notification::BlockTxsOffer(eb) => {
self.overlay.txs_offered += 1;
- let total = self.ebs.get(eb).and_then(|r| r.tx_total);
- if let Some(row) = self.ebs.get_mut(eb) {
+ let total = {
+ let row = self.upsert_eb(eb);
row.advance(EbStage::TxsOffered);
- }
+ row.tx_total
+ };
match total {Also applies to: 284-303
🤖 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 `@examples/leios-tui/src/dashboard.rs` around lines 218 - 225, The
`EbFetched::Block` handling in `dashboard.rs` only updates `tx_total` and
advances the row, so if `BlockTxsOffer` arrived first the offer is never acted
on. Update the block-body path to preserve offers via `upsert_eb` and, when
`body.raw_bytes()` reveals the tx count, immediately trigger the first tx fetch
window for that existing row; also make sure the same behavior is applied in the
other body-handling block referenced by the review.
| leiosnotify::Notification::BlockOffer(eb, size) => { | ||
| self.overlay.offered += 1; | ||
| self.upsert_eb(eb).size = Some(*size); | ||
| actions.push(Action::FetchEb(pid.clone(), eb.clone())); | ||
| tracing::info!(eb = %fmt_eb(eb), size, "eb offered → fetching body"); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Avoid refetching EB bodies on duplicate offers.
BlockOffer always emits FetchEb, even for an EB row that already has a body request in flight or a fetched body. Gate the fetch on the first body offer to avoid redundant network calls.
Proposed fix
leiosnotify::Notification::BlockOffer(eb, size) => {
self.overlay.offered += 1;
- self.upsert_eb(eb).size = Some(*size);
- actions.push(Action::FetchEb(pid.clone(), eb.clone()));
- tracing::info!(eb = %fmt_eb(eb), size, "eb offered → fetching body");
+ let should_fetch = {
+ let row = self.upsert_eb(eb);
+ let should_fetch = row.size.is_none() && row.tx_total.is_none();
+ row.size = Some(*size);
+ should_fetch
+ };
+ if should_fetch {
+ actions.push(Action::FetchEb(pid.clone(), eb.clone()));
+ tracing::info!(eb = %fmt_eb(eb), size, "eb offered → fetching body");
+ } else {
+ tracing::info!(eb = %fmt_eb(eb), size, "eb offered (already known)");
+ }
}📝 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.
| leiosnotify::Notification::BlockOffer(eb, size) => { | |
| self.overlay.offered += 1; | |
| self.upsert_eb(eb).size = Some(*size); | |
| actions.push(Action::FetchEb(pid.clone(), eb.clone())); | |
| tracing::info!(eb = %fmt_eb(eb), size, "eb offered → fetching body"); | |
| leiosnotify::Notification::BlockOffer(eb, size) => { | |
| self.overlay.offered += 1; | |
| let should_fetch = { | |
| let row = self.upsert_eb(eb); | |
| let should_fetch = row.size.is_none() && row.tx_total.is_none(); | |
| row.size = Some(*size); | |
| should_fetch | |
| }; | |
| if should_fetch { | |
| actions.push(Action::FetchEb(pid.clone(), eb.clone())); | |
| tracing::info!(eb = %fmt_eb(eb), size, "eb offered → fetching body"); | |
| } else { | |
| tracing::info!(eb = %fmt_eb(eb), size, "eb offered (already known)"); | |
| } | |
| } |
🤖 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 `@examples/leios-tui/src/dashboard.rs` around lines 278 - 282, The BlockOffer
handling in dashboard.rs always queues Action::FetchEb, which causes duplicate
body requests for the same EB even when a fetch is already in flight or the body
is already present. Update the leiosnotify::Notification::BlockOffer branch in
dashboard::Dashboard logic to check the EB state before pushing FetchEb, using
the existing upsert_eb/EB tracking fields to detect whether this is the first
body offer. Only emit the fetch action when the EB has not yet had a body
request or stored body, while keeping the offered counter and logging unchanged.
| loop { | ||
| select! { | ||
| _ = self.housekeeping_interval.tick() => { | ||
| self.network.execute(InitiatorCommand::Housekeeping); | ||
| } | ||
| _ = self.render_interval.tick() => { | ||
| terminal.draw(|f| ui::draw(f, &self.dashboard, &mut self.table_state))?; | ||
| } | ||
| evt = self.network.poll_next() => { | ||
| if let Some(evt) = evt { | ||
| self.handle_event(evt); | ||
| } | ||
| } | ||
| ev = self.input.next() => { | ||
| if let Some(Ok(ev)) = ev | ||
| && self.dashboard.handle_input(ev) | ||
| { | ||
| return Ok(()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
pallas-network2 Manager poll_next behavior after peer disconnect
💡 Result:
In the pallas-network2 crate, the Manager struct acts as the orchestrator between a network Interface and a protocol Behavior [1][2][3]. When a peer disconnects, the behavior of Manager::poll_next is defined by how it reconciles the interface's IO events with the behavior's business logic [1][3]. The Manager::poll_next method advances the state machine by polling the network interface for events and subsequently driving the behavior [1][3]. When a peer disconnects at the transport level, the Interface yields an InterfaceEvent (typically InterfaceEvent::Disconnected) [1][4][3]. The Manager processes this event by passing it to the associated Behavior [1][2]. The Behavior is then responsible for updating its internal state (e.g., removing the peer from its registry) and may emit a corresponding event or output back to the external consumer [5][6]. Recent updates to the crate explicitly support tracking and reporting peer disconnections, where Behaviors like ResponderBehavior may emit a specific event (e.g., ResponderEvent::PeerDisconnected) to notify the application layer [5]. Consequently, when you poll the Manager, you receive these outputs emitted by the Behavior as it processes the underlying disconnection [1][5]. Essentially, Manager::poll_next does not unilaterally "handle" the disconnect by itself; rather, it facilitates the flow of the disconnection event from the Interface to the Behavior, and then surfaces any resulting outputs or events generated by the Behavior to your application's event loop [1][3].
Citations:
- 1: https://github.com/txpipe/pallas/tree/main/pallas-network2
- 2: https://docs.rs/pallas-network2/latest/pallas_network2/?search=IntersectResponse
- 3: https://crates.io/crates/pallas-network2
- 4: https://docs.rs/pallas-network2
- 5: e7a6058
- 6: 0dcec8f
🌐 Web query:
crossterm EventStream next returns None when stdin closes behavior
💡 Result:
In the crossterm crate, the EventStream::next method returns Poll::Ready(None) to indicate that the stream is exhausted [1][2]. When standard input (stdin) is closed, the behavior of EventStream is not explicitly documented as a standard "stream termination" signal in the same way a file or network socket might behave [1][3]. Instead, EventStream is designed to monitor terminal input (typically via TTY) [4][3]. Because the underlying implementation relies on polling a TTY file descriptor or equivalent source, it generally expects the terminal to remain open for the duration of the application's event loop [2][4][5]. If stdin is closed (e.g., via EOF), the internal polling mechanism may simply fail to retrieve further events or, depending on the platform and configuration, cause the polling thread to encounter errors [2][6][4]. It does not inherently guarantee a graceful stream termination (yielding None) upon stdin closure [1][2]. Key points regarding EventStream behavior: 1. Stream Exhaustion: As a type implementing the futures::Stream trait, returning None is the standard way to signal that a stream will no longer produce items [1]. 2. TTY Dependency: Crossterm's event system is tightly coupled with terminal TTY access [7][4][5]. It is not designed to function as a generic wrapper for arbitrary stdin pipes, and attempting to use it in environments without a proper TTY can lead to errors or panics [7][5]. 3. Implementation Details: The EventStream internally spawns a thread to poll for events [2][6]. If the input source closes, the internal poll logic may return an error rather than a controlled termination of the stream [2][4]. For robust applications, it is often recommended to monitor the status of the input stream separately or handle potential I/O errors that occur when the underlying terminal resource becomes unavailable [3]. If you require specific behavior upon stdin closure, you may need to implement custom handling or checks before the next poll [8].
Citations:
- 1: https://docs.rs/crossterm/latest/crossterm/event/struct.EventStream.html
- 2: https://github.com/crossterm-rs/crossterm/blob/master/src/event/stream.rs
- 3: https://docs.rs/crossterm/latest/crossterm/event/index.html
- 4: https://github.com/crossterm-rs/crossterm/blob/6af9116b/src/event/source/unix/tty.rs
- 5: Ability to customize event source crossterm-rs/crossterm#694
- 6: https://github.com/crossterm-rs/crossterm/blob/6af9116b/src/event/stream.rs
- 7: A way to change the input stream for event::read and event::poll crossterm-rs/crossterm#728
- 8: How to temporarily lock EventStream polling crossterm-rs/crossterm#1039
🌐 Web query:
tokio_stream StreamExt next returns None when stream ends behavior
💡 Result:
In the tokio_stream crate, the StreamExt::next method is used to consume and return the next value in a stream [1][2]. When the stream is finished, next returns None [1][2]. Key behaviors regarding this process include: 1. Stream Termination: Receiving None from next indicates that the stream has been exhausted or terminated [3]. 2. Undefined Behavior Post-Termination: Once a stream has returned None, its behavior is generally undefined if polled further [1][4]. Future calls to poll_next may result in the stream returning Some(T) again, returning None, or even panicking, depending on the specific implementation of the stream [1][4]. 3. Fuse for Safety: If you need to ensure that a stream consistently returns None after it has finished, you should use the fuse adapter [1][4]. The fuse method ensures that after the stream returns None for the first time, all subsequent calls to next will also return None [1][4]. 4. Requirements: Because next does not take ownership of the stream, the stream type must satisfy the Unpin trait [1][2]. If a stream is not Unpin, it must be pinned (for example, using Box::pin or the pin_mut! macro) before calling next [1][2]. This behavior is typically utilized in while let loops to iterate through stream items until the stream terminates [3][5].
Citations:
- 1: https://docs.rs/tokio-stream/latest/tokio_stream/trait.StreamExt.html
- 2: https://docs.rs/tokio-stream/latest/src/tokio_stream/stream_ext.rs.html
- 3: https://tokio.rs/tokio/tutorial/streams
- 4: https://docs.rs/tokio/0.3.5/tokio/stream/trait.StreamExt.html
- 5: https://docs.rs/tokio-stream/latest/tokio_stream/index.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== main.rs excerpt ==\n'
sed -n '100,145p' examples/leios-tui/src/main.rs
printf '\n== Cargo manifest references ==\n'
rg -n 'crossterm|tokio-stream|pallas-network2|ratatui' -S Cargo.toml examples/leios-tui/Cargo.toml Cargo.lock || trueRepository: txpipe/pallas
Length of output: 2215
🌐 Web query:
crossterm EventStream docs None end of stream
💡 Result:
In the context of the Crossterm library, the EventStream implements the Stream trait, which defines a poll_next method that returns Poll<Option<Self::Item>> [1][2]. When this method returns None, it signifies that the stream is exhausted (i.e., the end of the stream has been reached) [1][2]. However, for a standard crossterm::event::EventStream, the stream is essentially an infinite source of terminal events and does not naturally "end" or terminate in typical usage [1][3]. If you are observing None in your code (e.g., when pattern matching on the result of reader.next().await), it usually indicates that the stream has been dropped or that a specific implementation detail of the surrounding async runtime or wrapper has caused the stream to terminate [4][5]. Key points regarding EventStream and stream exhaustion: 1. Nature of the Stream: EventStream is designed to continuously yield terminal events (keyboard, mouse, etc.) [3]. It is not a finite collection of data that naturally finishes [1]. 2. Handling None: In async code using tokio::select! or similar mechanisms, receiving None from EventStream::next() typically means the stream has been closed or dropped [4][5]. If you are seeing this unexpectedly, check your code to ensure the EventStream instance is not being dropped prematurely [6]. 3. Implementation: The poll_next implementation returns Poll::Pending when no events are currently available, keeping the stream alive [6][7]. It does not return Poll::Ready(None) under normal operation unless the underlying internal reader is no longer functional or the stream has been shut down [6]. If you are following a pattern like the one found in common tutorials (e.g., in Ratatui applications), the event loop usually runs indefinitely [5]. If your loop exits due to None, investigate where the EventStream instance is being held and if it is being dropped by a scope change or error condition [6][5].
Citations:
- 1: https://docs.rs/crossterm/latest/crossterm/event/struct.EventStream.html
- 2: https://docs.rs/crate/crossterm/0.29.0/target-redirect/crossterm/event/struct.EventStream.html
- 3: https://docs.rs/crossterm/latest/crossterm/event/index.html
- 4: Document cancellation safety of
EventStreamcrossterm-rs/crossterm#936 - 5: https://github.com/ratatui/ratatui-website/blob/main/src/content/docs/tutorials/counter-async-app/async-event-stream.md
- 6: https://github.com/crossterm-rs/crossterm/blob/master/src/event/stream.rs
- 7: https://github.com/crossterm-rs/crossterm/blob/6af9116b/src/event/stream.rs
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('examples/leios-tui/src/main.rs')
for i, line in enumerate(p.read_text().splitlines(), 1):
if 110 <= i <= 140:
print(f"{i:4d}: {line}")
PYRepository: txpipe/pallas
Length of output: 1412
🌐 Web query:
crossterm eventstream stream ends None closed stdin docs.rs
💡 Result:
In the crossterm crate, the EventStream type implements the futures::stream::Stream trait, yielding Result<Event, std::io::Error> items [1][2]. Regarding the behavior of the stream: 1. Stream End (None): When using EventStream, receiving None from the stream signifies that the stream has been exhausted or terminated [1][3][4]. In typical usage within an asynchronous event loop (e.g., using StreamExt::next), a None result is the standard indicator to break the loop [5][6]. 2. Stdin and Closed Streams: EventStream relies on the terminal's input source [2]. If the underlying input (stdin/TTY) is closed or cannot be accessed, the behavior often depends on the environment [7][8]. Historically, issues have been raised regarding EventStream panicking during initialization or polling if a valid TTY is not available, rather than gracefully returning an error or closing the stream [7][8]. Because EventStream is tightly coupled with terminal event processing, it does not function as a generic stream that simply terminates if stdin is closed; rather, it expects an active TTY connection [2][9]. To use EventStream, you must enable the event-stream feature flag in your Cargo.toml [1][2]. It is recommended to handle the Option<Result<Event>> returned by the stream by checking for both Some(Err(_)) (to handle I/O errors) and None (to handle graceful termination) [5][6].
Citations:
- 1: https://docs.rs/crossterm/latest/crossterm/event/struct.EventStream.html
- 2: https://github.com/crossterm-rs/crossterm/blob/master/src/event/stream.rs
- 3: https://docs.rs/crossterm/0.23.0/crossterm/event/struct.EventStream.html
- 4: https://docs.rs/crossterm/0.22.1/crossterm/event/struct.EventStream.html
- 5: https://github.com/crossterm-rs/crossterm/blob/master/examples/event-stream-async-std.rs
- 6: Document cancellation safety of
EventStreamcrossterm-rs/crossterm#936 - 7: Return Result from InternalEventReader crossterm-rs/crossterm#659
- 8: Ability to customize event source crossterm-rs/crossterm#694
- 9: https://docs.rs/crossterm/latest/crossterm/event/index.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== poll_next definitions and stream impls ==\n'
rg -n "poll_next\\s*\\(|impl .*Stream|type Item|Disconnected|None" pallas-network2 examples/leios-tui -S
printf '\n== candidate files ==\n'
fd -a ".*(manager|stream|event|behavior).*\\.rs$" pallas-network2Repository: txpipe/pallas
Length of output: 13815
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [Path('pallas-network2'), Path('examples/leios-tui')]:
if path.exists():
print(f'== {path} ==')
for p in path.rglob('*.rs'):
txt = p.read_text(errors='ignore')
if 'poll_next' in txt or 'Disconnected' in txt or 'impl Stream' in txt:
print(p)
PYRepository: txpipe/pallas
Length of output: 664
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for p in Path('pallas-network2').rglob('*.rs'):
txt = p.read_text(errors='ignore')
if 'poll_next' in txt or 'Disconnected' in txt or 'impl Stream' in txt:
print(p)
PYRepository: txpipe/pallas
Length of output: 586
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== pallas-network2/src/lib.rs (280-360) ==\n'
sed -n '280,360p' pallas-network2/src/lib.rs
printf '\n== pallas-network2/src/interface.rs (250-360) ==\n'
sed -n '250,360p' pallas-network2/src/interface.rs
printf '\n== pallas-network2/src/behavior/mod.rs (1-140) ==\n'
sed -n '1,140p' pallas-network2/src/behavior/mod.rsRepository: txpipe/pallas
Length of output: 11850
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== pallas-network2/README.md relevant excerpts ==\n'
sed -n '30,70p' pallas-network2/README.md
printf '\n== examples/leios-tui/src/main.rs top and input init ==\n'
sed -n '1,110p' examples/leios-tui/src/main.rsRepository: txpipe/pallas
Length of output: 5687
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [Path('pallas-network2/src/lib.rs'), Path('pallas-network2/src/interface.rs'), Path('pallas-network2/src/behavior/mod.rs')]:
print(f'\n### {path} ###')
txt = path.read_text()
for start in [1]:
pass
PYRepository: txpipe/pallas
Length of output: 274
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
def show(path, start, end):
print(f"\n== {path} ({start}-{end}) ==")
lines = path.read_text().splitlines()
for i in range(start, min(end, len(lines)) + 1):
print(f"{i:4d}: {lines[i-1]}")
show(Path('pallas-network2/src/lib.rs'), 280, 360)
show(Path('pallas-network2/src/interface.rs'), 250, 360)
show(Path('pallas-network2/src/behavior/mod.rs'), 1, 140)
PYRepository: txpipe/pallas
Length of output: 13843
Handle input errors from EventStream. self.input.next() yields Option<Result<Event, Error>>, but the current branch drops Some(Err(_)), so terminal/input failures are silently ignored and the loop keeps running on a broken source.
🤖 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 `@examples/leios-tui/src/main.rs` around lines 113 - 135, The input handling in
the main event loop is ignoring `Some(Err(_))` from `EventStream::next`, so
failures are silently dropped. Update the `select!` branch in `main` to
explicitly handle the `self.input.next()` result in `leios-tui::main` by
matching `Option<Result<Event, Error>>`, returning an error or otherwise
surfacing the failure when the stream yields `Err`, while keeping the existing
`dashboard.handle_input` path for valid events.
| let len = d.ebs.len(); | ||
| if len == 0 { | ||
| table_state.select(None); | ||
| } else { | ||
| let sel = if d.follow { 0 } else { d.selected.min(len - 1) }; | ||
| table_state.select(Some(sel)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect how `selected` is mutated relative to new EB insertion order.
rg -n -A5 -B5 'selected' examples/leios-tui/src/dashboard.rsRepository: txpipe/pallas
Length of output: 1339
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect insertion order for `ebs` and surrounding selection logic.
sed -n '160,260p' examples/leios-tui/src/dashboard.rs
printf '\n---\n'
sed -n '330,390p' examples/leios-tui/src/dashboard.rs
printf '\n---\n'
sed -n '130,195p' examples/leios-tui/src/ui.rsRepository: txpipe/pallas
Length of output: 9358
Keep the selected EB stable when new rows arrive
selected is a positional index into d.ebs.values().rev(), so inserting a new EB while follow is off can shift the highlight to a different block. Track the selected EbId instead, or adjust the index when the list changes.
🤖 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 `@examples/leios-tui/src/ui.rs` around lines 186 - 192, The selection logic in
ui.rs is using a positional index for d.selected against d.ebs.values().rev(),
so adding new EB rows can move the highlight to the wrong item when follow is
off. Update the state handling around the table_state.select block to track the
selected EbId in the UI state, or recompute d.selected when the EB list changes
so the same block remains selected after inserts. Keep the fix centered on the
selection update path that uses d.ebs, d.follow, and table_state.select.
Reframe the dashboard as an educational view of the Leios overlay: ranking blocks and endorser blocks render as boxes in two tip-following lanes sharing one column axis, so an EB sits under the RB it belongs to and the shared column is the connection. The EB→RB association is a slot heuristic (nearest following RB) since no on-wire message links them; BlockAnnouncement decoding is left as a hook for when the devnet diffuses announcements. EB boxes show tx-download and vote bars plus a per-phase checklist; RB boxes show a derived block hash and slot. Also refreshes the chain-sync intersect point to a current tip.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@examples/leios-tui/src/ui.rs`:
- Around line 369-370: The labels in the UI are using `row.voters.len()` as raw
“votes”, which can be misleading when vote metadata is missing and causes the EB
status to disagree with the displayed count. Update the label formatting in the
`Line::from(...)` rendering for this row (and the similar block around the other
affected location) to separate raw vote totals from distinct voter counts, using
the lifecycle’s `row.votes` for votes and a distinct label for voter count. Keep
the `full_bar(row.vote_ratio(scale), ...)` rendering unchanged, and make sure
the label text in the UI reflects the actual meaning of each field.
- Line 493: The footer text in ui.rs is missing the documented Up/Down selection
keys, even though the dashboard supports them. Update the footer string in the
UI rendering logic to advertise both ↑/↓ and ←/→ selection controls, keeping the
existing follow/clear-log/help wording intact.
🪄 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
Run ID: 2e93c037-89f7-47f9-87f3-f344194f733d
📒 Files selected for processing (4)
examples/leios-tui/Cargo.tomlexamples/leios-tui/src/dashboard.rsexamples/leios-tui/src/main.rsexamples/leios-tui/src/ui.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- examples/leios-tui/Cargo.toml
- examples/leios-tui/src/main.rs
| Line::from(format!("votes {}/{}", row.voters.len(), scale)), | ||
| full_bar(row.vote_ratio(scale), bar_w, Color::Green), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Separate raw votes from distinct voters in labels.
These lines label row.voters.len() as votes, while the lifecycle uses row.votes. If a vote lacks voter metadata, the EB can show “voting” while displaying votes 0.
Proposed label fix
- Line::from(format!("votes {}/{}", row.voters.len(), scale)),
+ Line::from(format!("voters {}/{}", row.voters.len(), scale)),
full_bar(row.vote_ratio(scale), bar_w, Color::Green),- " selected EB {hash} · slot {} · {size} · txs {txs} · votes {} · ",
+ " selected EB {hash} · slot {} · {size} · txs {txs} · voters {} · votes {} · ",
group(row.slot),
- row.voters.len()
+ row.voters.len(),
+ row.votesAlso applies to: 451-453
🤖 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 `@examples/leios-tui/src/ui.rs` around lines 369 - 370, The labels in the UI
are using `row.voters.len()` as raw “votes”, which can be misleading when vote
metadata is missing and causes the EB status to disagree with the displayed
count. Update the label formatting in the `Line::from(...)` rendering for this
row (and the similar block around the other affected location) to separate raw
vote totals from distinct voter counts, using the lifecycle’s `row.votes` for
votes and a distinct label for voter count. Keep the
`full_bar(row.vote_ratio(scale), ...)` rendering unchanged, and make sure the
label text in the UI reflects the actual meaning of each field.
| fn render_footer(f: &mut Frame, d: &Dashboard, area: Rect) { | ||
| let follow = if d.follow { "on" } else { "off" }; | ||
| let line = Line::from(format!( | ||
| " q quit ←/→ select EB f follow:{follow} c clear log · column = nearest RB by slot (heuristic) · vote bar ∝ peak voters" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Include the documented Up/Down selection keys in the footer.
The dashboard handles ↑/↓ as well as ←/→, but the footer only advertises ←/→.
- " q quit ←/→ select EB f follow:{follow} c clear log · column = nearest RB by slot (heuristic) · vote bar ∝ peak voters"
+ " q quit ↑/↓ or ←/→ select EB f follow:{follow} c clear log · column = nearest RB by slot (heuristic) · vote bar ∝ peak voters"📝 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.
| " q quit ←/→ select EB f follow:{follow} c clear log · column = nearest RB by slot (heuristic) · vote bar ∝ peak voters" | |
| " q quit ↑/↓ or ←/→ select EB f follow:{follow} c clear log · column = nearest RB by slot (heuristic) · vote bar ∝ peak voters" |
🤖 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 `@examples/leios-tui/src/ui.rs` at line 493, The footer text in ui.rs is
missing the documented Up/Down selection keys, even though the dashboard
supports them. Update the footer string in the UI rendering logic to advertise
both ↑/↓ and ←/→ selection controls, keeping the existing follow/clear-log/help
wording intact.
What
Adds a new
leios-tuiexample: a live terminal dashboard that turns a Leios initiator node into an educational visualization of the overlay.It runs the same flow as the
leios-testnetexample — negotiate the v15 (Leios) handshake, follow Praos chain-sync, and fetch Endorser Blocks overleios-notify/leios-fetch— but renders it as aratatuiTUI instead of log lines.The view
Ranking blocks (RBs) and endorser blocks (EBs) render as boxes in two vertically-aligned swim lanes sharing one column axis: the newest RBs define the columns (newest at the tip, right), and each EB is drawn in the column of the RB it belongs to — so the shared column is the connection. The window slides as the tip advances.
slot, a tx-download bar and a vote bar (label + value above each full-width bar), and a per-phase checklist (offered/body/download txs/votes) flagged with✓done,◐in progress,○pending.On the EB→RB association
There is no on-wire link between an EB and its RB today:
leios_certin the Dijkstra header is an empty placeholder andBlockAnnouncementis unspecified (any). So the column assignment is a slot heuristic (nearest RB withslot >= eb_slot), clearly labelled as such in the footer. DecodingBlockAnnouncementfor a real link is left as a documented hook for when the devnet diffuses announcements.The vote bar is likewise self-calibrating (voters against the peak seen), since votes carry no stake weight and there is no committee/quorum on the wire; real quorum is stake-weighted τ≈75% per CIP-0164.
Layout
src/main.rs— network setup + thetokio::select!loop (housekeeping, render tick, network events, keyboard input).src/dashboard.rs— network-free view state; foldsInitiatorEvents into state, emits follow-upActions, and performs RB retention + EB→RB linking.src/logbuf.rs— atracinglayer capturing recent log lines into an in-memory buffer for the log panel.src/ui.rs— the aligned swim-laneratatuirendering (unit-tested headlessly viaTestBackend).Also registers the example as a workspace member in the root
Cargo.tomland adds apallas-cryptodependency for RB hashing.Run with:
```sh
cargo run -p leios-tui
```
Keys: `q` quit · `←`/`→` select EB · `f` toggle follow-tip · `c` clear log.
Notes
🤖 Generated with Claude Code
Summary by CodeRabbit