Skip to content

Commit 82dfa25

Browse files
committed
v2.5.2: fix filter UI extra-writes on scroll (alt-screen + viewport)
1 parent 28fa08e commit 82dfa25

1 file changed

Lines changed: 76 additions & 24 deletions

File tree

codex-md.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22
"""
3-
Codex Session Manager & Markdown Converter (v2.5.1)
3+
Codex Session Manager & Markdown Converter (v2.5.2)
44
-------------------------------------------------
55
An interactive tool to browse, filter, and convert OpenAI Codex
66
session logs (.jsonl) into readable Markdown documents.
@@ -1053,9 +1053,17 @@ def get_lines_for_state(cap_out: int, cap_user: int, cap_agent: int, cap_reason:
10531053
ROW_INTERNAL = len(SECTION_DEFS) + 5
10541054
num_items = len(SECTION_DEFS) + 6
10551055

1056-
sys.stdout.write('\033[?25l')
1056+
import shutil as _shutil
1057+
1058+
# Alternate screen buffer (htop/vim/less style) — keeps the filter UI off
1059+
# the terminal's scrollback so it can't accumulate frames there. We combine
1060+
# that with an INTERNAL viewport: when the filter has more rows than the
1061+
# terminal can show, we render only the slice around the cursor and surface
1062+
# ▲/▼ markers so you always know there's more.
1063+
sys.stdout.write('\033[?1049h\033[?25l\033[H\033[2J')
10571064
sys.stdout.flush()
1058-
_clear_screen()
1065+
1066+
scroll_offset = 0
10591067

10601068
try:
10611069
while True:
@@ -1064,13 +1072,9 @@ def get_lines_for_state(cap_out: int, cap_user: int, cap_agent: int, cap_reason:
10641072
selected_lines = sum(agg_lines.get(s[0], 0) for s in SECTION_DEFS if fstate.get(s[0], False))
10651073
pct = (selected_lines / total_lines * 100) if total_lines > 0 else 0
10661074

1067-
# ── Render ──
1068-
out = ['\033[H']
1069-
sessions_label = f"{len(parsers)} session{'s' if len(parsers) > 1 else ''}"
1070-
if scope_label:
1071-
sessions_label += f" · {scope_label}"
1072-
out.append(f"\n {Style.BOLD}{Style.HEADER}SECTION FILTER{Style.RESET} {Style.DIM}({sessions_label}){Style.RESET}")
1073-
out.append(f" {Style.DIM}{'━' * 62}{Style.RESET}\n")
1075+
# ── Build the LIST of navigable / decorative middle rows ──
1076+
# Each entry is (cursor_id_or_None, rendered_text).
1077+
mid_rows: List[Tuple[Optional[int], str]] = []
10741078

10751079
for i, (key, name, emoji, _default) in enumerate(SECTION_DEFS):
10761080
is_cursor = (i == cursor)
@@ -1099,9 +1103,9 @@ def get_lines_for_state(cap_out: int, cap_user: int, cap_agent: int, cap_reason:
10991103
pad_len = max(1, 44 - len(visible_name))
11001104
dots = f'{Style.DIM}{"·" * pad_len}{Style.RESET}'
11011105

1102-
out.append(f' {arrow} {toggle} {nstyle}{visible_name}{Style.RESET} {dots} {count_str}{msg_label}')
1106+
mid_rows.append((i, f' {arrow} {toggle} {nstyle}{visible_name}{Style.RESET} {dots} {count_str}{msg_label}'))
11031107

1104-
out.append(f' {Style.DIM}{"─" * 62}{Style.RESET}')
1108+
mid_rows.append((None, f' {Style.DIM}{"─" * 62}{Style.RESET}'))
11051109

11061110
# Clean Chat
11071111
cc_on = clean_content
@@ -1111,58 +1115,104 @@ def get_lines_for_state(cap_out: int, cap_user: int, cap_agent: int, cap_reason:
11111115
cc_st = f'{Style.BOLD}' if cc_cur else Style.DIM
11121116
cc_val = f'{Style.GREEN}ON {Style.RESET}' if cc_on else f'{Style.DIM}OFF{Style.RESET}'
11131117
chat_lines = agg_lines.get('user_message', 0) + agg_lines.get('agent_message', 0)
1114-
out.append(f' {cc_arrow} {cc_tog} {cc_st}✂️ Clean Chat{Style.RESET} {Style.DIM}(strips IDE context from 👤🤖){Style.RESET} {Style.DIM}{chat_lines:,}L{Style.RESET} {cc_val}')
1118+
mid_rows.append((ROW_CLEAN, f' {cc_arrow} {cc_tog} {cc_st}✂️ Clean Chat{Style.RESET} {Style.DIM}(strips IDE context from 👤🤖){Style.RESET} {Style.DIM}{chat_lines:,}L{Style.RESET} {cc_val}'))
11151119

11161120
# Output Cap
11171121
cap_cur = (cursor == ROW_CAP)
11181122
cap_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if cap_cur else ' '
11191123
cap_st = f'{Style.BOLD}' if cap_cur else Style.DIM
11201124
cap_label = f'{Style.DIM}ALL{Style.RESET}' if output_cap == 0 else f'{Style.YELLOW}{output_cap}{Style.RESET}'
11211125
hint = f' {Style.DIM}◀▶{Style.RESET}' if cap_cur else ''
1122-
out.append(f' {cap_arrow} {cap_st}💻 Terminal Output Cap{Style.RESET} {Style.DIM}(max lines/block){Style.RESET} {cap_label}{hint}')
1126+
mid_rows.append((ROW_CAP, f' {cap_arrow} {cap_st}💻 Terminal Output Cap{Style.RESET} {Style.DIM}(max lines/block){Style.RESET} {cap_label}{hint}'))
11231127

11241128
# User Cap
11251129
u_cur = (cursor == ROW_USER)
11261130
u_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if u_cur else ' '
11271131
u_st = f'{Style.BOLD}' if u_cur else Style.DIM
11281132
u_label = f'{Style.DIM}ALL{Style.RESET}' if user_cap == 0 else f'{Style.YELLOW}Last {user_cap}{Style.RESET}'
11291133
u_hint = f' {Style.DIM}◀▶{Style.RESET}' if u_cur else ''
1130-
out.append(f' {u_arrow} {u_st}👤 User Message Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {u_label}{u_hint}')
1134+
mid_rows.append((ROW_USER, f' {u_arrow} {u_st}👤 User Message Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {u_label}{u_hint}'))
11311135

11321136
# Agent Cap
11331137
a_cur = (cursor == ROW_AGENT)
11341138
a_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if a_cur else ' '
11351139
a_st = f'{Style.BOLD}' if a_cur else Style.DIM
11361140
a_label = f'{Style.DIM}ALL{Style.RESET}' if agent_cap == 0 else f'{Style.YELLOW}Last {agent_cap}{Style.RESET}'
11371141
a_hint = f' {Style.DIM}◀▶{Style.RESET}' if a_cur else ''
1138-
out.append(f' {a_arrow} {a_st}🤖 Agent Message Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {a_label}{a_hint}')
1142+
mid_rows.append((ROW_AGENT, f' {a_arrow} {a_st}🤖 Agent Message Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {a_label}{a_hint}'))
11391143

11401144
# Reason Cap
11411145
r_cur = (cursor == ROW_REASON)
11421146
r_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if r_cur else ' '
11431147
r_st = f'{Style.BOLD}' if r_cur else Style.DIM
11441148
r_label = f'{Style.DIM}ALL{Style.RESET}' if reason_cap == 0 else f'{Style.YELLOW}Last {reason_cap}{Style.RESET}'
11451149
r_hint = f' {Style.DIM}◀▶{Style.RESET}' if r_cur else ''
1146-
out.append(f' {r_arrow} {r_st}🧠 Agent Reasoning Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {r_label}{r_hint}')
1150+
mid_rows.append((ROW_REASON, f' {r_arrow} {r_st}🧠 Agent Reasoning Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {r_label}{r_hint}'))
11471151

11481152
# Internal Reasoning Cap
11491153
i_cur = (cursor == ROW_INTERNAL)
11501154
i_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if i_cur else ' '
11511155
i_st = f'{Style.BOLD}' if i_cur else Style.DIM
11521156
i_label = f'{Style.DIM}ALL{Style.RESET}' if internal_cap == 0 else f'{Style.YELLOW}Last {internal_cap}{Style.RESET}'
11531157
i_hint = f' {Style.DIM}◀▶{Style.RESET}' if i_cur else ''
1154-
out.append(f' {i_arrow} {i_st}🔒 Internal Reasoning Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {i_label}{i_hint}')
1158+
mid_rows.append((ROW_INTERNAL, f' {i_arrow} {i_st}🔒 Internal Reasoning Cap{Style.RESET} {Style.DIM}(blocks){Style.RESET} {i_label}{i_hint}'))
1159+
1160+
# ── Viewport math ──
1161+
term_size = _shutil.get_terminal_size((80, 30))
1162+
term_h = max(10, term_size.lines)
1163+
# Fixed-size header (3 lines: blank + title + separator) +
1164+
# fixed-size footer (separator + progress bar + hint + 1 cushion).
1165+
# Plus 2 lines reserved for ▲/▼ markers so the layout never jumps.
1166+
HEADER_LINES = 3
1167+
FOOTER_LINES = 5
1168+
INDICATOR_LINES = 2
1169+
view_h = max(5, term_h - HEADER_LINES - FOOTER_LINES - INDICATOR_LINES)
1170+
1171+
# Find cursor's index in mid_rows and scroll viewport so it's visible
1172+
cur_pos = next((idx for idx, (rid, _) in enumerate(mid_rows) if rid == cursor), 0)
1173+
if cur_pos < scroll_offset:
1174+
scroll_offset = cur_pos
1175+
elif cur_pos >= scroll_offset + view_h:
1176+
scroll_offset = cur_pos - view_h + 1
1177+
max_offset = max(0, len(mid_rows) - view_h)
1178+
scroll_offset = max(0, min(scroll_offset, max_offset))
1179+
1180+
# ── Compose frame ──
1181+
out = ['\033[H\033[2J'] # home + clear (alt-screen surface)
1182+
sessions_label = f"{len(parsers)} session{'s' if len(parsers) > 1 else ''}"
1183+
if scope_label:
1184+
sessions_label += f" · {scope_label}"
1185+
out.append(f" {Style.BOLD}{Style.HEADER}SECTION FILTER{Style.RESET} {Style.DIM}({sessions_label}){Style.RESET}")
1186+
out.append(f" {Style.DIM}{'━' * 62}{Style.RESET}")
1187+
out.append('')
1188+
1189+
# ▲ indicator (always reserve the line — keeps row positions stable)
1190+
if scroll_offset > 0:
1191+
out.append(f' {Style.DIM}{scroll_offset} more above{Style.RESET}')
1192+
else:
1193+
out.append('')
1194+
1195+
# Viewport slice
1196+
for _rid, text in mid_rows[scroll_offset:scroll_offset + view_h]:
1197+
out.append(text)
1198+
1199+
# ▼ indicator
1200+
below = max(0, len(mid_rows) - (scroll_offset + view_h))
1201+
if below > 0:
1202+
out.append(f' {Style.DIM}{below} more below{Style.RESET}')
1203+
else:
1204+
out.append('')
11551205

1156-
out.append(f'\n {Style.DIM}{"━" * 62}{Style.RESET}')
1206+
# Footer: separator + progress + hint
1207+
out.append(f' {Style.DIM}{"━" * 62}{Style.RESET}')
11571208
bar_w = 30
11581209
filled = int(bar_w * pct / 100)
11591210
bar = f'{Style.GREEN}{"█" * filled}{Style.DIM}{"░" * (bar_w - filled)}{Style.RESET}'
11601211
sel_c = Style.GREEN if pct > 0 else Style.RED
11611212
out.append(f' {bar} {sel_c}{Style.BOLD}{selected_lines:,}{Style.RESET}{Style.DIM}/{Style.RESET}{total_lines:,} {Style.DIM}({pct:.0f}%){Style.RESET}')
1162-
out.append(f'\n {Style.DIM}↑↓ move ⏎ toggle ◀▶ cap Q export A all N none D defaults 1-7 presets{Style.RESET}')
1163-
out.append('\033[J')
1213+
out.append(f' {Style.DIM}↑↓ move ⏎ toggle ◀▶ cap Q export A all N none D defaults 1-7 presets{Style.RESET}')
11641214

1165-
sys.stdout.write('\n'.join(out) + '\n')
1215+
sys.stdout.write('\n'.join(out))
11661216
sys.stdout.flush()
11671217

11681218
key = read_key()
@@ -1238,7 +1288,9 @@ def get_lines_for_state(cap_out: int, cap_user: int, cap_agent: int, cap_reason:
12381288
for s in SECTION_DEFS: fstate[s[0]] = s[0] in pkeys
12391289
clean_content = pclean
12401290
finally:
1241-
sys.stdout.write('\033[?25h')
1291+
# Restore cursor and leave the alt screen. The terminal pops back to
1292+
# whatever was on the main screen before we entered.
1293+
sys.stdout.write('\033[?25h\033[?1049l')
12421294
sys.stdout.flush()
12431295

12441296
return fstate, clean_content, output_cap, user_cap, agent_cap, reason_cap, internal_cap
@@ -1307,7 +1359,7 @@ def get_all_sessions() -> List[Path]:
13071359

13081360
def print_menu_header():
13091361
os.system('cls' if os.name == 'nt' else 'clear')
1310-
print(f"\n{Style.BOLD}CODEX SESSION MANAGER{Style.RESET} {Style.DIM}v2.5.1{Style.RESET}")
1362+
print(f"\n{Style.BOLD}CODEX SESSION MANAGER{Style.RESET} {Style.DIM}v2.5.2{Style.RESET}")
13111363
print(f"{Style.DIM}Directory: {SESSIONS_DIR}{Style.RESET}")
13121364
print(f"{Style.DIM}Output: {Path(__file__).parent.resolve()}{Style.RESET}\n")
13131365

0 commit comments

Comments
 (0)