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-------------------------------------------------
55An interactive tool to browse, filter, and convert OpenAI Codex
66session 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
13081360def 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