From: Christian Brauner <brauner@kernel.org>
To: "Kernel.org Tools" <tools@kernel.org>
Cc: Konstantin Ryabitsev <konstantin@linuxfoundation.org>,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH RFC 10/11] review-tui: expand tracked series into per-version rows
Date: Sat, 18 Jul 2026 00:37:46 +0200 [thread overview]
Message-ID: <20260718-work-b4-multiver-rows-v1-10-3c539d2a3095@kernel.org> (raw)
In-Reply-To: <20260718-work-b4-multiver-rows-v1-0-3c539d2a3095@kernel.org>
Series with more than one known version become expandable: 'x'
unfolds indented child rows (one per catalog revision, the tracked
one starred) showing per-version unread badges and activity dates,
'X' toggles every series at once. Enter or 'e' on a child opens that
revision's thread with revision-correct seen syncing, 'd' range-diffs
it against the tracked revision directly, and every other action
keeps operating on the parent series. Expansion state and the
(change_id, revision) cursor position survive the periodic DB-mtime
reloads and limit filtering.
Assisted-by: LLM
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
src/b4/review_tui/_modals.py | 7 +
src/b4/review_tui/_tracking_app.py | 282 ++++++++++++++++++++++++++++++++++---
2 files changed, 266 insertions(+), 23 deletions(-)
diff --git a/src/b4/review_tui/_modals.py b/src/b4/review_tui/_modals.py
index 5943426..32f92a3 100644
--- a/src/b4/review_tui/_modals.py
+++ b/src/b4/review_tui/_modals.py
@@ -328,9 +328,16 @@ TRACKING_HELP_LINES = [
' [bold]d[/bold] Range-diff between revisions\n',
' [bold]a[/bold] Open action menu (take, rebase, etc.)\n',
' [bold]u[/bold] Update selected series\n',
+ ' [bold]x[/bold] Expand/collapse the version rows of a ▸ series\n',
+ '\n',
+ '[bold]Version rows[/bold]\n',
+ ' v2* Asterisk marks the revision the series tracks\n',
+ ' [bold]Enter[/bold] / [bold]e[/bold] View the thread of that version\n',
+ ' [bold]d[/bold] Range-diff that version against the tracked one\n',
'\n',
'[bold]App[/bold]\n',
' [bold]U[/bold] Update all tracked series\n',
+ ' [bold]X[/bold] Expand/collapse all multi-version series\n',
' [bold]l[/bold] Filter series by pattern\n',
' [bold]s[/bold] Suspend to shell\n',
' [bold]p[/bold] Switch to Patchwork TUI\n',
diff --git a/src/b4/review_tui/_tracking_app.py b/src/b4/review_tui/_tracking_app.py
index 8800383..3135781 100644
--- a/src/b4/review_tui/_tracking_app.py
+++ b/src/b4/review_tui/_tracking_app.py
@@ -334,6 +334,27 @@ def _resolve_worktree_take_conflict(
return True
+def _format_version(rev: Dict[str, Any], series: Dict[str, Any]) -> str:
+ """Summarize one known version of *series* for the details panel."""
+ revision = rev.get('revision', 1)
+ out = f'v{revision}'
+ if revision == series.get('revision', 1):
+ out += ' (tracked)'
+ count = rev.get('message_count')
+ if count is None:
+ out += ' — - msgs (- unseen)'
+ else:
+ unseen = max(0, int(count) - int(rev.get('seen_message_count') or 0))
+ out += f' — {count} msgs ({unseen} unseen)'
+ found_at = rev.get('found_at') or ''
+ if found_at:
+ out += f', found {found_at[:10]}'
+ last_activity = rev.get('last_activity_at') or ''
+ if last_activity:
+ out += f', last activity {last_activity[:10]}'
+ return out
+
+
def _format_snooze_until(value: str) -> str:
"""Format a snoozed_until value for display.
@@ -576,9 +597,10 @@ class TrackedSeriesItem(ListItem):
}
"""
- def __init__(self, series: Dict[str, Any]) -> None:
+ def __init__(self, series: Dict[str, Any], expanded: bool = False) -> None:
super().__init__()
self.series = series
+ self.expanded = expanded
status = series.get('status', 'new')
effective = 'queued' if series.get('queued') else status
if _STATUS_TIER.get(effective, 2) >= 2:
@@ -607,6 +629,11 @@ class TrackedSeriesItem(ListItem):
width = len(str(num_patches)) if num_patches > 0 else 1
parts = extras + [f'v{revision}', f'{"0" * width}/{num_patches:0{width}d}']
subject_display = f'[{",".join(parts)}] {ls.subject}'
+ # Series with more than one known version can be expanded with [x]
+ if len(TrackingApp._merge_tracked_revision(self.series)) > 1:
+ # U+25BE black down-pointing / U+25B8 black right-pointing triangle
+ marker = '▾' if self.expanded else '▸'
+ subject_display = f'{marker} {subject_display}'
if display_width(submitter) > 20:
while display_width(submitter) > 19:
submitter = submitter[:-1]
@@ -633,6 +660,68 @@ class TrackedSeriesItem(ListItem):
yield Label(label, markup=False)
+class TrackedRevisionItem(ListItem):
+ """A single known version of an expanded series.
+
+ Rendered as a child row underneath its TrackedSeriesItem, with the
+ submitter column replaced by a tree glyph and the version number.
+ """
+
+ DEFAULT_CSS = """
+ TrackedRevisionItem Label {
+ text-style: dim;
+ }
+ """
+
+ def __init__(
+ self,
+ series: Dict[str, Any],
+ rev: Dict[str, Any],
+ is_tracked: bool,
+ is_last: bool,
+ ) -> None:
+ super().__init__()
+ self.series = series
+ self.rev = rev
+ self.is_tracked = is_tracked
+ self.is_last = is_last
+
+ def compose(self) -> ComposeResult:
+ # U+2514/U+251C box drawings light up-and-right / vertical-and-right
+ tree = '└─' if self.is_last else '├─'
+ mark = '*' if self.is_tracked else ''
+ version = f' {tree} v{self.rev.get("revision", 1)}{mark}'
+ # Date of the last known activity, falling back to when the
+ # revision was first seen.
+ date_str = ''
+ stamp = self.rev.get('last_activity_at') or self.rev.get('found_at') or ''
+ if stamp:
+ try:
+ date_str = datetime.datetime.fromisoformat(stamp).strftime('%d %b')
+ except (ValueError, TypeError):
+ date_str = ''
+ # Show each version's own title, prefix-stripped -- the version
+ # column already carries the vN part. Fall back to the series
+ # subject when the catalog has none.
+ rev_subject = self.rev.get('subject') or self.series.get('subject') or ''
+ if rev_subject:
+ rev_subject = b4.LoreSubject(rev_subject).subject
+ label = RichText(no_wrap=True, overflow='ellipsis')
+ label.append(pad_display(version, 20))
+ # Attestation (1), separator (1) and A·R·T (7) stay blank
+ label.append(' ' * 9)
+ _append_msgs(
+ label,
+ self.app,
+ self.rev.get('message_count'),
+ self.rev.get('seen_message_count'),
+ )
+ label.append(f' {date_str:<6s}')
+ if rev_subject:
+ label.append(f' {rev_subject}')
+ yield Label(label, markup=False)
+
+
class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
"""Textual app for browsing tracked series.
@@ -731,10 +820,12 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
'check': 'Series',
'thread': 'Series',
'range_diff': 'Series',
+ 'toggle_expand': 'Series',
'action': 'Series',
'update_one': 'Series',
'target_branch': 'Series',
'update_all': 'App',
+ 'expand_all': 'App',
'process_queue': 'App',
'limit': 'App',
'suspend': 'App',
@@ -756,7 +847,9 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
Binding('a', 'action', 'action'),
Binding('u', 'update_one', 'update'),
Binding('d', 'range_diff', 'range-diff'),
+ Binding('x', 'toggle_expand', 'versions'),
# App-global actions
+ Binding('X', 'expand_all', 'Expand all', key_display='X', show=False),
Binding('l', 'limit', 'limit'),
Binding('s', 'suspend', 'shell'),
Binding('p', 'patchwork', 'patchwork'),
@@ -782,6 +875,11 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
self._patatt_sign = patatt_sign
self._all_series: List[Dict[str, Any]] = []
self._selected_series: Optional[Dict[str, Any]] = None
+ # Series showing per-version child rows, and the child row (if any)
+ # the cursor is on — both survive a list rebuild.
+ self._expanded_change_ids: set[str] = set()
+ self._selected_revision: Optional[Dict[str, Any]] = None
+ self._focus_revision: Optional[int] = None
self._limit_pattern: str = ''
self._db_mtime: float = 0.0
# Detect patchwork configuration
@@ -829,6 +927,16 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
self._cached_revisions = None
self._cached_art_counts = None
+ def _stash_focus(self) -> None:
+ """Remember the highlighted row so the next _refresh_list restores it."""
+ if self._selected_series:
+ self._focus_change_id = self._selected_series.get('change_id')
+ self._focus_revision = (
+ self._selected_revision.get('revision')
+ if self._selected_revision is not None
+ else None
+ )
+
def _refresh_msg_count(self, series: Dict[str, Any], total_messages: int) -> None:
"""Opportunistically refresh message count after fetching messages."""
if not self._identifier:
@@ -872,6 +980,9 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
with Horizontal(classes='details-row', id='detail-revisions-row'):
yield Static('Revisions:', classes='details-label')
yield Static('', id='detail-revisions', markup=False)
+ with Horizontal(classes='details-row', id='detail-version-row'):
+ yield Static('Version:', classes='details-label')
+ yield Static('', id='detail-version', markup=False)
with Horizontal(classes='details-row', id='detail-branch-row'):
yield Static('Branch:', classes='details-label')
yield Static('', id='detail-branch', markup=False)
@@ -950,8 +1061,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
# pick up the change once the modal closes.
if len(self.app.screen_stack) > 1:
return
- if self._selected_series:
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self._invalidate_caches()
self._load_series()
@@ -1141,8 +1251,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
except OSError:
return
if mtime != self._db_mtime:
- if self._selected_series:
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self._invalidate_caches()
self._load_series()
@@ -1216,7 +1325,26 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
header_text = f'{"Submitter":<20s}{"A":>1s} {"A·R·T":>7s} {"Msgs":>6s} {"S":<4s}{"Subject"}'
header = Static(header_text, id='tracking-header')
- list_items: List[ListItem] = [TrackedSeriesItem(s) for s in display_series]
+ list_items: List[ListItem] = []
+ for series in display_series:
+ revs = self._merge_tracked_revision(series)
+ expanded = (
+ len(revs) > 1
+ and series.get('change_id', '') in self._expanded_change_ids
+ )
+ list_items.append(TrackedSeriesItem(series, expanded=expanded))
+ if not expanded:
+ continue
+ tracked = series.get('revision', 1)
+ for idx, rev in enumerate(revs):
+ list_items.append(
+ TrackedRevisionItem(
+ series,
+ rev,
+ is_tracked=rev.get('revision') == tracked,
+ is_last=idx == len(revs) - 1,
+ )
+ )
lv = ListView(*list_items, id='tracking-list')
await self.mount(header, before=self.query_one(Footer))
await self.mount(lv, before=self.query_one(Footer))
@@ -1228,8 +1356,11 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
and item.series.get('change_id') == self._focus_change_id
):
lv.index = idx
+ if self._focus_revision is not None:
+ lv.index = self._find_focus_child(list_items, idx)
break
self._focus_change_id = None
+ self._focus_revision = None
lv.focus()
# Populate the details panel for the highlighted item now that
@@ -1239,10 +1370,30 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
# status without waiting for the async Highlighted message to
# be processed from the queue.
highlighted = lv.highlighted_child
- if isinstance(highlighted, TrackedSeriesItem):
+ if isinstance(highlighted, TrackedRevisionItem):
+ self._selected_series = highlighted.series
+ self._selected_revision = highlighted.rev
+ self._show_details(highlighted.series, rev=highlighted.rev)
+ elif isinstance(highlighted, TrackedSeriesItem):
self._selected_series = highlighted.series
+ self._selected_revision = None
self._show_details(highlighted.series)
+ def _find_focus_child(self, list_items: List[ListItem], parent_idx: int) -> int:
+ """Index of the _focus_revision child row below *parent_idx*.
+
+ Falls back to the parent index when that version is no longer
+ listed — it may have gone away, or the series may have been
+ collapsed since the focus was stashed.
+ """
+ for idx in range(parent_idx + 1, len(list_items)):
+ item = list_items[idx]
+ if not isinstance(item, TrackedRevisionItem):
+ break
+ if item.rev.get('revision') == self._focus_revision:
+ return idx
+ return parent_idx
+
def action_limit(self) -> None:
self.push_screen(
LimitScreen(
@@ -1255,8 +1406,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
if result is None:
return
self._limit_pattern = result
- if self._selected_series:
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self._load_series()
def action_cursor_down(self) -> None:
@@ -1271,17 +1421,58 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
except Exception:
pass
+ def action_toggle_expand(self) -> None:
+ """Show or hide the version rows of the selected series."""
+ series = self._selected_series
+ if not series or len(self._merge_tracked_revision(series)) < 2:
+ return
+ change_id = series.get('change_id', '')
+ if change_id in self._expanded_change_ids:
+ self._expanded_change_ids.discard(change_id)
+ # The highlighted version row is about to go away
+ self._selected_revision = None
+ else:
+ self._expanded_change_ids.add(change_id)
+ self._stash_focus()
+ self.call_later(self._refresh_list)
+
+ def action_expand_all(self) -> None:
+ """Expand every multi-version series, or collapse them all."""
+ expandable = {
+ s.get('change_id', '')
+ for s in self._all_series
+ if len(self._merge_tracked_revision(s)) > 1
+ }
+ if not expandable:
+ return
+ if expandable - self._expanded_change_ids:
+ self._expanded_change_ids |= expandable
+ else:
+ self._expanded_change_ids.clear()
+ self._selected_revision = None
+ self._stash_focus()
+ self.call_later(self._refresh_list)
+
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
if event.list_view.id != 'tracking-list':
return
item = event.item
- if isinstance(item, TrackedSeriesItem):
+ if isinstance(item, TrackedRevisionItem):
+ self._selected_series = item.series
+ self._selected_revision = item.rev
+ self._show_details(item.series, rev=item.rev)
+ elif isinstance(item, TrackedSeriesItem):
self._selected_series = item.series
+ self._selected_revision = None
self._show_details(item.series)
self.refresh_bindings()
def on_list_view_selected(self, event: ListView.Selected) -> None:
if event.list_view.id == 'tracking-list':
+ # A version row opens that version's thread
+ if isinstance(event.item, TrackedRevisionItem):
+ self.action_thread()
+ return
if not self._selected_series:
return
status = self._selected_series.get('status', 'new')
@@ -1355,6 +1546,11 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
return bool(self._pwkey and self._pwurl and self._pwproj)
if action == 'action':
return self._selected_series is not None
+ if action == 'toggle_expand':
+ return bool(
+ self._selected_series
+ and len(self._merge_tracked_revision(self._selected_series)) > 1
+ )
if action in self._GATED_ACTIONS:
if not self._selected_series:
return False
@@ -1581,10 +1777,19 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
self._checkout_new_series()
def action_thread(self) -> None:
- """View a series thread in the lite thread viewer."""
+ """View a series thread in the lite thread viewer.
+
+ A highlighted version row views that version's thread; otherwise
+ the thread of the revision the series tracks.
+ """
if not self._selected_series:
return
- message_id = self._selected_series.get('message_id', '')
+ source = (
+ self._selected_revision
+ if self._selected_revision is not None
+ else self._selected_series
+ )
+ message_id = source.get('message_id', '')
if not message_id:
self.notify('No message-id available for this series', severity='error')
return
@@ -1593,10 +1798,10 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
tracking_info = {
'identifier': self._identifier,
'change_id': self._selected_series.get('change_id', ''),
- 'revision': self._selected_series.get('revision', 1),
- 'is_rethreaded': bool(self._selected_series.get('is_rethreaded')),
+ 'revision': source.get('revision', 1),
+ 'is_rethreaded': bool(source.get('is_rethreaded')),
}
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
from b4.review_tui._lite_app import LiteThreadScreen
self.push_screen(
@@ -1608,7 +1813,8 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
)
)
- def _merge_tracked_revision(self, series: Dict[str, Any]) -> List[Dict[str, Any]]:
+ @staticmethod
+ def _merge_tracked_revision(series: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Known revisions of a series, guaranteed to include the tracked one.
The revisions catalog is not guaranteed a row for the tracked
@@ -2042,8 +2248,14 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
# Exit to review mode
self.exit(branch_name)
- def _show_details(self, series: Dict[str, Any]) -> None:
+ def _show_details(
+ self, series: Dict[str, Any], rev: Optional[Dict[str, Any]] = None
+ ) -> None:
+ """Fill the details panel for *series*, or for one of its versions.
+ With *rev* given the version-specific fields (subject, link) come
+ from that revision instead of the tracked one.
+ """
try:
panel = self.query_one('#details-panel', Vertical)
except NoMatches:
@@ -2051,6 +2263,9 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
raw_subject = series.get('subject', '(no subject)')
revision = series.get('revision', 1)
+ if rev is not None:
+ raw_subject = rev.get('subject') or raw_subject
+ revision = rev.get('revision', revision)
num_patches = series.get('num_patches', 0) or 0
ls = b4.LoreSubject(raw_subject)
extras = ls.get_extra_prefixes(exclude=['patch'])
@@ -2066,7 +2281,10 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
# Create link URL from message-id using linkmask
link_url = ''
- if message_id:
+ if rev is not None:
+ link_url = rev.get('link') or ''
+ message_id = rev.get('message_id', '')
+ if not link_url and message_id:
config = b4.get_main_config()
linkmask = config.get('linkmask', b4.LOREADDR + '/%s')
if isinstance(linkmask, str) and '%s' in linkmask:
@@ -2149,6 +2367,16 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
else:
revisions_row.display = False
+ # Describe the highlighted version row, if any
+ version_row = self.query_one('#detail-version-row', Horizontal)
+ if rev is not None:
+ self.query_one('#detail-version', Static).update(
+ _format_version(rev, series)
+ )
+ version_row.display = True
+ else:
+ version_row.display = False
+
# Show branch name for series with a review branch
status = series.get('status', 'new')
branch_row = self.query_one('#detail-branch-row', Horizontal)
@@ -2324,7 +2552,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
linkmask = str(config.get('linkmask', 'https://lore.kernel.org/r/%s'))
topdir = b4.git_get_toplevel()
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self.push_screen(
UpdateAllScreen(
[self._selected_series], self._identifier, linkmask, topdir
@@ -2345,8 +2573,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
# Skip snoozed series during update-all
update_list = [s for s in self._all_series if s.get('status') != 'snoozed']
- if self._selected_series:
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self.push_screen(
UpdateAllScreen(update_list, self._identifier, linkmask, topdir),
callback=self._on_update_complete,
@@ -2389,6 +2616,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
if self._selected_series is not None:
panel.styles.height = 0
self._selected_series = None
+ self._selected_revision = None
def action_take(self) -> None:
"""Show take options dialog for the selected series."""
@@ -3589,6 +3817,15 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
change_id = self._selected_series.get('change_id', '')
current_rev = self._selected_series.get('revision', 1)
+ # A highlighted version row already names the other side of the
+ # diff, so skip the picker.
+ if self._selected_revision is not None:
+ other_rev = self._selected_revision.get('revision')
+ if other_rev is not None and other_rev != current_rev:
+ with self.suspend():
+ self._do_range_diff(change_id, current_rev, other_rev)
+ return
+
try:
conn = b4.review.tracking.get_db(self._identifier)
revisions = b4.review.tracking.get_revisions(conn, change_id)
@@ -5107,8 +5344,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
self.notify(', '.join(parts) if parts else 'Queue empty')
self._refresh_queue_indicator()
if delivered_series:
- if self._selected_series:
- self._focus_change_id = self._selected_series.get('change_id')
+ self._stash_focus()
self._load_series()
self.push_screen(
--
2.53.0
next prev parent reply other threads:[~2026-07-17 22:38 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-17 22:37 [PATCH RFC 00/11] review: track and browse every version of a tracked series Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 01/11] review-tui: fix rethreaded series thread viewing Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 02/11] review: track message counts for all revisions of a series Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 03/11] review: test per-revision message tracking Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 04/11] review-tui: poll every revision on u/U updates Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 05/11] review-tui: guarantee the tracked revision in revision lists Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 06/11] review: add backward discovery of older series revisions Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 07/11] review-tui: add a "Find older revisions" action Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 08/11] review: test backward revision discovery Christian Brauner
2026-07-17 22:37 ` [PATCH RFC 09/11] review-tui: extract the Msgs column renderer from TrackedSeriesItem Christian Brauner
2026-07-17 22:37 ` Christian Brauner [this message]
2026-07-17 22:37 ` [PATCH RFC 11/11] review-tui: test per-version tracker rows Christian Brauner
2026-07-27 20:43 ` [PATCH RFC 00/11] review: track and browse every version of a tracked series Konstantin Ryabitsev
2026-07-27 21:27 ` Christian Brauner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260718-work-b4-multiver-rows-v1-10-3c539d2a3095@kernel.org \
--to=brauner@kernel.org \
--cc=konstantin@linuxfoundation.org \
--cc=tools@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox