Skip to content

Commit dfd2b9e

Browse files
author
MCP
committed
feat: add Last N chat message filtering (user/agent/reasoning) independent of terminal cap
1 parent 656d67b commit dfd2b9e

1 file changed

Lines changed: 135 additions & 119 deletions

File tree

codex-md.py

Lines changed: 135 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ def count_lines_by_section(self) -> Dict[str, int]:
636636

637637
# --- markdown rendering with filter ---
638638
def to_markdown(self, section_filter: Optional[Dict[str, bool]] = None,
639-
clean_content: bool = False, output_cap: int = 0) -> str:
639+
clean_content: bool = False, output_cap: int = 0, msg_cap: int = 0) -> str:
640640
if section_filter is None:
641641
section_filter = {s[0]: True for s in SECTION_DEFS}
642642

@@ -650,6 +650,15 @@ def _cap_text(text: str) -> str:
650650
kept = lines[-output_cap:]
651651
return f'... ({len(lines) - output_cap} lines trimmed) ...\n' + '\n'.join(kept)
652652

653+
keep_indices = set(range(len(self.data)))
654+
if msg_cap > 0:
655+
chat_msg_count = 0
656+
for i in range(len(self.data) - 1, -1, -1):
657+
if self.data[i]['type'] in {'user_message', 'agent_message', 'agent_reasoning', 'reasoning'}:
658+
if chat_msg_count >= msg_cap:
659+
keep_indices.remove(i)
660+
chat_msg_count += 1
661+
653662
md: List[str] = []
654663
md.append(f"# {self.title}\n")
655664
last_rendered_message = None
@@ -664,7 +673,8 @@ def _cap_text(text: str) -> str:
664673
md.append(f"CWD: {self.metadata['cwd']}")
665674
md.append("```\n")
666675

667-
for item in self.data:
676+
for i, item in enumerate(self.data):
677+
if i not in keep_indices: continue
668678
itype = item['type']
669679
if not section_filter.get(itype, False):
670680
continue
@@ -782,66 +792,96 @@ def read_key() -> str:
782792
# ──────────────────────────────────────────────────────────────
783793

784794
# Output sections whose blocks can be capped
785-
OUTPUT_SECTIONS = {'terminal_output', 'mcp_tool_output', 'other_tool_output',
786-
'custom_tool_output'}
795+
OUTPUT_SECTIONS = {'terminal_output', 'mcp_tool_output', 'other_tool_output', 'custom_tool_output'}
787796
# Cap steps for ◀ ▶ control (0 = no cap, show all)
788797
CAP_STEPS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 50, 100, 200, 500]
798+
MSG_CAP_STEPS = [0, 5, 10, 20, 50] + list(range(70, 511, 20))
789799

790-
def interactive_filter(parsers: List[SessionParser]) -> Tuple[Dict[str, bool], bool, int]:
800+
def interactive_filter(parsers: List[SessionParser]) -> Tuple[Dict[str, bool], bool, int, int]:
791801
"""
792802
Full-screen interactive filter.
793-
Returns (section_filter, clean_content, output_cap).
803+
Returns (section_filter, clean_content, output_cap, msg_cap).
794804
"""
795-
# ── Pre-compute data ──
796-
agg_lines: Dict[str, int] = {}
797-
for parser in parsers:
798-
for key, count in parser.count_lines_by_section().items():
799-
agg_lines[key] = agg_lines.get(key, 0) + count
805+
_line_cache = {}
806+
807+
def get_lines_for_state(cap_out: int, cap_msg: int, cc: bool) -> Dict[str, int]:
808+
cache_key = (cap_out, cap_msg, cc)
809+
if cache_key in _line_cache:
810+
return _line_cache[cache_key]
811+
812+
counts = {s[0]: 0 for s in SECTION_DEFS}
813+
tool_call_types = {'terminal_cmd', 'mcp_tool', 'other_tool'}
814+
tool_output_types = {'terminal_output', 'mcp_tool_output', 'other_tool_output'}
815+
816+
for parser in parsers:
817+
keep_indices = set(range(len(parser.data)))
818+
if cap_msg > 0:
819+
chat_msg_count = 0
820+
for i in range(len(parser.data) - 1, -1, -1):
821+
if parser.data[i]['type'] in {'user_message', 'agent_message', 'agent_reasoning', 'reasoning'}:
822+
if chat_msg_count >= cap_msg:
823+
keep_indices.remove(i)
824+
chat_msg_count += 1
825+
826+
for i, item in enumerate(parser.data):
827+
if i not in keep_indices: continue
828+
itype = item['type']
829+
content = item.get('content', '')
830+
831+
if itype == 'user_message':
832+
if cc: content = trim_chat_content(content)
833+
lines = content.count('\n') + 4 if content else 0
834+
elif itype == 'agent_message':
835+
if cc: content = trim_chat_content(content)
836+
lines = content.count('\n') + 4 if content else 0
837+
elif itype in ('agent_reasoning', 'reasoning'):
838+
lines = 2 if not content else content.count('\n') + 3
839+
elif itype in tool_call_types:
840+
args = item.get('arguments', '')
841+
lines = args.count('\n') + 5
842+
elif itype in tool_output_types:
843+
out = item.get('output', '')
844+
total_out = out.count('\n') + 1 if out.strip() else 0
845+
if cap_out > 0: lines = min(total_out, cap_out) + 4 if total_out > 0 else 0
846+
else: lines = total_out + 4 if total_out > 0 else 0
847+
elif itype == 'custom_tool_call':
848+
lines = content.count('\n') + 5
849+
elif itype == 'custom_tool_output':
850+
total_out = content.count('\n') + 1 if content.strip() else 0
851+
if cap_out > 0: lines = min(total_out, cap_out) + 4 if total_out > 0 else 0
852+
else: lines = total_out + 4 if total_out > 0 else 0
853+
elif itype in ('web_search', 'token_count', 'turn_context', 'task_event', 'git_snapshot', 'session_event'):
854+
lines = 2
855+
elif itype == 'system_message':
856+
lines = content.count('\n') + 5
857+
else:
858+
lines = 1
859+
860+
counts[itype] = counts.get(itype, 0) + lines
861+
862+
if parser.metadata:
863+
counts['session_meta'] = counts.get('session_meta', 0) + 5
864+
865+
_line_cache[cache_key] = counts
866+
return counts
800867

801-
# Per-output-block content line counts (for cap estimation)
802-
output_blocks: Dict[str, List[int]] = {}
803-
for parser in parsers:
804-
for item in parser.data:
805-
itype = item['type']
806-
if itype in OUTPUT_SECTIONS:
807-
text = item.get('output', item.get('content', ''))
808-
if text.strip():
809-
output_blocks.setdefault(itype, []).append(text.count('\n') + 1)
810-
811-
# Lines affected by clean chat (user + agent messages)
812-
chat_lines = agg_lines.get('user_message', 0) + agg_lines.get('agent_message', 0)
813-
814-
def capped_lines_for(section_key: str, cap: int) -> int:
815-
"""Compute output section lines with a cap applied."""
816-
if cap == 0:
817-
return agg_lines.get(section_key, 0)
818-
blocks = output_blocks.get(section_key, [])
819-
return sum(min(cl, cap) + 4 for cl in blocks)
820-
821-
def effective_lines(section_key: str) -> int:
822-
"""Line count for a section, respecting current output_cap."""
823-
if section_key in OUTPUT_SECTIONS and output_cap > 0:
824-
return capped_lines_for(section_key, output_cap)
825-
return agg_lines.get(section_key, 0)
826-
827-
# ── State ──
828868
fstate: Dict[str, bool] = {s[0]: s[3] for s in SECTION_DEFS}
829869
clean_content = False
830-
output_cap = 8 # default: 8 lines per output block
870+
output_cap = 8
831871
cap_idx = CAP_STEPS.index(8)
872+
msg_cap = 0
873+
msg_idx = MSG_CAP_STEPS.index(0)
832874

833875
cursor = 0
834-
# Rows: sections + clean_chat + output_cap
835876
ROW_CLEAN = len(SECTION_DEFS)
836877
ROW_CAP = len(SECTION_DEFS) + 1
837-
num_items = len(SECTION_DEFS) + 2
878+
ROW_MSG = len(SECTION_DEFS) + 2
879+
num_items = len(SECTION_DEFS) + 3
838880

839881
while True:
840-
# ── Compute totals ──
841-
total_lines = sum(effective_lines(s[0]) for s in SECTION_DEFS)
842-
selected_lines = sum(
843-
effective_lines(s[0]) for s in SECTION_DEFS if fstate.get(s[0], False)
844-
)
882+
agg_lines = get_lines_for_state(output_cap, msg_cap, clean_content)
883+
total_lines = sum(agg_lines.get(s[0], 0) for s in SECTION_DEFS)
884+
selected_lines = sum(agg_lines.get(s[0], 0) for s in SECTION_DEFS if fstate.get(s[0], False))
845885
pct = (selected_lines / total_lines * 100) if total_lines > 0 else 0
846886

847887
# ── Render ──
@@ -853,48 +893,25 @@ def effective_lines(section_key: str) -> int:
853893
for i, (key, name, emoji, _default) in enumerate(SECTION_DEFS):
854894
is_cursor = (i == cursor)
855895
is_on = fstate.get(key, False)
856-
lines = effective_lines(key)
857-
full = agg_lines.get(key, 0)
858-
896+
lines = agg_lines.get(key, 0)
897+
859898
arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if is_cursor else ' '
899+
toggle = f'{Style.GREEN}██{Style.RESET}' if is_on else f'{Style.DIM}░░{Style.RESET}'
900+
901+
if is_cursor and is_on: nstyle = f'{Style.BOLD}{Style.GREEN}'
902+
elif is_cursor and not is_on: nstyle = f'{Style.BOLD}{Style.RED}'
903+
elif is_on: nstyle = ''
904+
else: nstyle = Style.DIM
860905

861-
if is_on:
862-
toggle = f'{Style.GREEN}██{Style.RESET}'
863-
else:
864-
toggle = f'{Style.DIM}░░{Style.RESET}'
865-
866-
if is_cursor and is_on:
867-
nstyle = f'{Style.BOLD}{Style.GREEN}'
868-
elif is_cursor and not is_on:
869-
nstyle = f'{Style.BOLD}{Style.RED}'
870-
elif is_on:
871-
nstyle = ''
872-
else:
873-
nstyle = Style.DIM
874-
875-
# Line count — show capped vs full if cap is active
876-
if key in OUTPUT_SECTIONS and output_cap > 0 and full != lines:
877-
if lines == 0:
878-
count_str = f'{Style.DIM} 0{Style.RESET}'
879-
elif is_on:
880-
count_str = f'{Style.CYAN}{lines:>6,}{Style.RESET}{Style.DIM}{full:,}{Style.RESET}'
881-
else:
882-
count_str = f'{Style.DIM}{lines:>6,}{full:,}{Style.RESET}'
883-
else:
884-
if lines == 0:
885-
count_str = f'{Style.DIM} 0{Style.RESET}'
886-
elif is_on:
887-
count_str = f'{Style.CYAN}{lines:>6,}{Style.RESET}'
888-
else:
889-
count_str = f'{Style.DIM}{lines:>6,}{Style.RESET}'
906+
count_str = f'{Style.CYAN}{lines:>6,}{Style.RESET}' if is_on and lines > 0 else f'{Style.DIM}{lines:>6,}{Style.RESET}'
907+
if lines == 0: count_str = f'{Style.DIM} 0{Style.RESET}'
890908

891909
visible_name = f'{emoji} {name}'
892910
pad_len = max(1, 44 - len(visible_name))
893911
dots = f'{Style.DIM}{"·" * pad_len}{Style.RESET}'
894912

895913
print(f' {arrow} {toggle} {nstyle}{visible_name}{Style.RESET} {dots} {count_str}')
896914

897-
# ── Settings rows ──
898915
print(f' {Style.DIM}{"─" * 62}{Style.RESET}')
899916

900917
# Clean Chat
@@ -904,85 +921,83 @@ def effective_lines(section_key: str) -> int:
904921
cc_tog = f'{Style.GREEN}██{Style.RESET}' if cc_on else f'{Style.DIM}░░{Style.RESET}'
905922
cc_st = f'{Style.BOLD}' if cc_cur else Style.DIM
906923
cc_val = f'{Style.GREEN}ON {Style.RESET}' if cc_on else f'{Style.DIM}OFF{Style.RESET}'
907-
print(f' {cc_arrow} {cc_tog} {cc_st}✂️ Clean Chat{Style.RESET}'
908-
f' {Style.DIM}(strips IDE context from 👤🤖){Style.RESET}'
909-
f' {Style.DIM}{chat_lines:,}L{Style.RESET} {cc_val}')
924+
chat_lines = agg_lines.get('user_message', 0) + agg_lines.get('agent_message', 0)
925+
print(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}')
910926

911927
# Output Cap
912928
cap_cur = (cursor == ROW_CAP)
913929
cap_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if cap_cur else ' '
914930
cap_st = f'{Style.BOLD}' if cap_cur else Style.DIM
915-
if output_cap == 0:
916-
cap_label = f'{Style.DIM}ALL{Style.RESET}'
917-
else:
918-
cap_label = f'{Style.YELLOW}{output_cap}{Style.RESET}'
931+
cap_label = f'{Style.DIM}ALL{Style.RESET}' if output_cap == 0 else f'{Style.YELLOW}{output_cap}{Style.RESET}'
919932
hint = f' {Style.DIM}◀▶{Style.RESET}' if cap_cur else ''
920-
print(f' {cap_arrow} {cap_st}📏 Output Cap{Style.RESET}'
921-
f' {Style.DIM}(max lines per block){Style.RESET}'
922-
f' {cap_label}{hint}')
933+
print(f' {cap_arrow} {cap_st}💻 Terminal Output Cap{Style.RESET} {Style.DIM}(max lines/block){Style.RESET} {cap_label}{hint}')
934+
935+
# Msg Cap
936+
msg_cur = (cursor == ROW_MSG)
937+
msg_arrow = f'{Style.BOLD}{Style.YELLOW}{Style.RESET}' if msg_cur else ' '
938+
msg_st = f'{Style.BOLD}' if msg_cur else Style.DIM
939+
msg_label = f'{Style.DIM}ALL{Style.RESET}' if msg_cap == 0 else f'{Style.YELLOW}Last {msg_cap}{Style.RESET}'
940+
msg_hint = f' {Style.DIM}◀▶{Style.RESET}' if msg_cur else ''
941+
print(f' {msg_arrow} {msg_st}🕒 Chat Message Cap{Style.RESET} {Style.DIM}(blocks for 👤🤖🧠){Style.RESET} {msg_label}{msg_hint}')
923942

924-
# ── Footer ──
925943
print(f'\n {Style.DIM}{"━" * 62}{Style.RESET}')
926944
bar_w = 30
927945
filled = int(bar_w * pct / 100)
928946
bar = f'{Style.GREEN}{"█" * filled}{Style.DIM}{"░" * (bar_w - filled)}{Style.RESET}'
929947
sel_c = Style.GREEN if pct > 0 else Style.RED
930-
print(f' {bar} {sel_c}{Style.BOLD}{selected_lines:,}{Style.RESET}'
931-
f'{Style.DIM}/{Style.RESET}{total_lines:,} {Style.DIM}({pct:.0f}%){Style.RESET}')
948+
print(f' {bar} {sel_c}{Style.BOLD}{selected_lines:,}{Style.RESET}{Style.DIM}/{Style.RESET}{total_lines:,} {Style.DIM}({pct:.0f}%){Style.RESET}')
932949
print(f'\n {Style.DIM}↑↓ move ⏎ toggle ◀▶ cap Q export A all N none D defaults 1-7 presets{Style.RESET}')
933950

934-
# ── Read key ──
935951
key = read_key()
936952

937-
if key == 'UP':
938-
cursor = (cursor - 1) % num_items
939-
elif key == 'DOWN':
940-
cursor = (cursor + 1) % num_items
953+
if key == 'UP': cursor = (cursor - 1) % num_items
954+
elif key == 'DOWN': cursor = (cursor + 1) % num_items
941955
elif key in ('ENTER', 'SPACE'):
942956
if cursor < len(SECTION_DEFS):
943957
skey = SECTION_DEFS[cursor][0]
944958
fstate[skey] = not fstate[skey]
945959
elif cursor == ROW_CLEAN:
946960
clean_content = not clean_content
947-
# ROW_CAP: enter does nothing — use ◀▶
948961
elif key == 'LEFT':
949-
cap_idx = max(0, cap_idx - 1)
950-
output_cap = CAP_STEPS[cap_idx]
962+
if cursor == ROW_CAP:
963+
cap_idx = max(0, cap_idx - 1)
964+
output_cap = CAP_STEPS[cap_idx]
965+
elif cursor == ROW_MSG:
966+
msg_idx = max(0, msg_idx - 1)
967+
msg_cap = MSG_CAP_STEPS[msg_idx]
951968
elif key == 'RIGHT':
952-
cap_idx = min(len(CAP_STEPS) - 1, cap_idx + 1)
953-
output_cap = CAP_STEPS[cap_idx]
969+
if cursor == ROW_CAP:
970+
cap_idx = min(len(CAP_STEPS) - 1, cap_idx + 1)
971+
output_cap = CAP_STEPS[cap_idx]
972+
elif cursor == ROW_MSG:
973+
msg_idx = min(len(MSG_CAP_STEPS) - 1, msg_idx + 1)
974+
msg_cap = MSG_CAP_STEPS[msg_idx]
954975
elif key == 'A':
955-
for s in SECTION_DEFS:
956-
fstate[s[0]] = True
976+
for s in SECTION_DEFS: fstate[s[0]] = True
957977
elif key == 'N':
958-
for s in SECTION_DEFS:
959-
fstate[s[0]] = False
978+
for s in SECTION_DEFS: fstate[s[0]] = False
960979
elif key == 'I':
961-
for s in SECTION_DEFS:
962-
fstate[s[0]] = not fstate[s[0]]
980+
for s in SECTION_DEFS: fstate[s[0]] = not fstate[s[0]]
963981
elif key == 'D':
964-
for s in SECTION_DEFS:
965-
fstate[s[0]] = s[3]
982+
for s in SECTION_DEFS: fstate[s[0]] = s[3]
966983
clean_content = False
967984
output_cap = 8
968985
cap_idx = CAP_STEPS.index(8)
969-
elif key == 'Q':
970-
break
971-
elif key == 'ESC':
986+
msg_cap = 0
987+
msg_idx = MSG_CAP_STEPS.index(0)
988+
elif key == 'Q' or key == 'ESC':
972989
break
973990
elif key.isdigit():
974991
pi = int(key) - 1
975992
if 0 <= pi < len(FILTER_PRESETS):
976993
_pname, pkeys, pclean = FILTER_PRESETS[pi]
977994
if pkeys is None:
978-
for s in SECTION_DEFS:
979-
fstate[s[0]] = s[3]
995+
for s in SECTION_DEFS: fstate[s[0]] = s[3]
980996
else:
981-
for s in SECTION_DEFS:
982-
fstate[s[0]] = s[0] in pkeys
997+
for s in SECTION_DEFS: fstate[s[0]] = s[0] in pkeys
983998
clean_content = pclean
984999

985-
return fstate, clean_content, output_cap
1000+
return fstate, clean_content, output_cap, msg_cap
9861001

9871002
# ──────────────────────────────────────────────────────────────
9881003
# Session List & Main Loop
@@ -1112,7 +1127,7 @@ def process_conversion(indices_str: str, files: List[Path]):
11121127
return
11131128

11141129
# Interactive filter
1115-
section_filter, clean_content, output_cap = interactive_filter(parsers)
1130+
section_filter, clean_content, output_cap, msg_cap = interactive_filter(parsers)
11161131

11171132
# Check anything is selected
11181133
if not any(section_filter.values()):
@@ -1149,6 +1164,7 @@ def process_conversion(indices_str: str, files: List[Path]):
11491164
section_filter=section_filter,
11501165
clean_content=clean_content,
11511166
output_cap=output_cap,
1167+
msg_cap=msg_cap,
11521168
)
11531169

11541170
date_prefix = datetime.fromtimestamp(

0 commit comments

Comments
 (0)