6Enhanced terminal presenter with statistics hooks and optional fixed header.
10Subclass of :class:`RichPresenter` that adds a thread-safe
11:class:`StatisticsTracker` for turn / session / context-usage counters
12and (when the terminal is interactive) installs a fixed-position
13ANSI-coloured header at the top of the scroll region. Selected via
18- :class:`EnhancedPresenter` — Rich-styled presenter with stats hooks.
19- ``_install_fixed_header`` / ``_redraw_fixed_header`` — manage the
20 fixed top region and re-render after each turn or context change.
21- Pass-through hooks for agent / sub-loop tracking that delegate to
22 :class:`StatisticsTracker`.
28 L2 — lifecycle events (init, close)
29 L3 — per-turn / context-usage updates
30 L4 — deep tracing (per-redraw decisions)
34- All rendering still goes through the parent :class:`RichPresenter`;
35 no presenter API surface is added or removed.
36- The fixed header is opt-in: skipped when ``stdout`` is not a TTY or
37 when ``OC_CHAT_ENHANCED_FIXED_HEADER`` is falsy.
38- ``close()`` always restores the terminal's full scroll region.
42- oc_diagnostics (structured debug logging via _dbg)
43- oc_ui.terminal.stats (StatisticsTracker)
44- oc_ui.terminal.rich_presenter (RichPresenter base class)
45- oc_chat.core.config (lazy; get_max_context_tokens for the context gauge window size)
46- oc_chat.core.formatting (lazy; format_duration for the fixed header turn and session durations)
47- oc_chat.core.session (lazy; ChatSession type hint for bind_session and show_welcome)
50from __future__
import annotations
54from typing
import Any, TYPE_CHECKING
56from oc_diagnostics
import _dbg
66 """Rich-styled presenter with enhanced statistics hooks."""
70 model: str |
None =
"deepseek-v4-pro",
71 max_context_size: int |
None =
None,
73 verbose: bool =
False,
76 func =
"EnhancedPresenter.__init__"
77 super().
__init__(verbose=verbose, **rich_kwargs)
84 if max_context_size
is None:
86 max_context_size = get_max_context_tokens(self.
_model)
89 self.
_stats.set_context_window_size(max_context_size)
95 and os.getenv(
"OC_CHAT_ENHANCED_FIXED_HEADER",
"1").lower()
96 not in {
"0",
"false",
"no",
"off"}
98 _dbg(
"OC_UI.TERMINAL", func,
"initialized", 2)
101 """Bind the active session and keep the display model + context window current.
103 Plan 134 N5: ``session.model`` is only known here (the presenter is
104 built before the session resolves), and it changes on /model /pro
105 /flash. When the window size is auto (no explicit --max-context-size),
106 refresh the gauge denominator from the real model so it agrees with the
107 turn-summary ctx:% gauge instead of diverging on a 128k default.
114 win = get_max_context_tokens(self.
_model)
116 self.
_stats.set_context_window_size(win)
120 """Display the welcome panel or install the fixed enhanced header."""
129 """Reserve a fixed top terminal region and draw the header there."""
130 columns, lines = shutil.get_terminal_size((100, 30))
136 self.
_console.file.write(
"\x1b[2J\x1b[H")
137 self.
_console.file.write(f
"\x1b[{self._fixed_header_height + 1};{lines}r")
138 self.
_console.file.write(f
"\x1b[{self._fixed_header_height + 1};1H")
143 """Redraw the fixed header without moving the scroll-region cursor."""
147 columns, lines = shutil.get_terminal_size((100, 30))
153 self.
_console.file.write(
"\x1b[1;1H")
156 if idx < len(header):
157 self.
_console.file.write(header[idx])
164 """Build ANSI-coloured header lines sized to the terminal width."""
166 stats = self.
_stats.get_stats_snapshot()
167 width = max(50, columns)
175 model = getattr(session,
"model", self.
_model)
or self.
_model
176 effort = getattr(session,
"reasoning_effort",
"")
177 name = getattr(session,
"name",
"session")
180 turn_total = stats[
"last_turn"][
"total"]
181 turn_tools = stats[
"last_turn"][
"tool_calls"]
182 turn_cache = stats[
"last_turn"][
"cache_hit_rate"]
183 sess_total = stats[
"session"][
"tokens"][
"total"]
184 sess_tools = stats[
"session"][
"timing"][
"tool_calls"]
185 sess_cache = stats[
"session"][
"tokens"][
"cache_hit_rate"]
186 context_pct = stats[
"context"][
"usage_percent"]
192 session_duration =
""
194 if status
is not None:
196 turn_duration = f
" · {format_duration(status.turn_elapsed_ms)}"
197 session_duration = f
" · {format_duration(status.session_elapsed_ms)}"
199 top_fill = max(0, inner - len(title))
200 top_left = top_fill // 2
201 top_right = top_fill - top_left
203 def cell(text: str =
"") -> str:
205 return f
"{blue}│{reset}{plain.ljust(inner)}{blue}│{reset}"
208 f
"{blue}╭{'─' * top_left}{title}{'─' * top_right}╮{reset}",
210 cell(f
" Model: {cyan}{model}{reset}"),
211 cell(f
" Reasoning effort: {cyan}{effort}{reset} {dim}(name: {name}){reset}"),
215 f
"{yellow}this turn:{reset} {self._fmt_compact_tokens(turn_total)} · "
216 f
"{turn_tools} tools · {turn_cache:.0f}% cache · ctx {context_pct:.0f}%{turn_duration} "
217 f
"{yellow}session:{reset} {self._fmt_compact_tokens(sess_total)} · "
218 f
"{sess_tools} tools · {sess_cache:.0f}% cache{session_duration}"
220 cell(f
" Type {yellow}/help{reset} for full command details and session subcommands."),
221 f
"{blue}╰{'─' * inner}╯{reset}",
229 duration_ms: int = 0,
232 """Update enhanced statistics after a turn or instrumentation event."""
242 func =
"update_turn_stats"
246 f
"updated: prompt={prompt}, completion={completion}, "
247 f
"cache_hits={cache_hits}, duration={duration_ms}ms",
258 prompt_cached: int |
None =
None,
260 cache_hit_pct: float |
None =
None,
261 cache_saved_tokens: int |
None =
None,
262 performance_stats: object =
None,
263 model: str |
None =
None,
264 context_used_pct: float |
None =
None,
265 duration_ms: int |
None =
None,
266 turn_cost: object =
None,
268 """Render the rich summary and mirror it into enhanced stats.
270 Plan 188 T4: *turn_cost* is pure pass-through — the stats tracker
271 counts tokens, and the ``~$`` estimate is rendered by the Rich base.
276 cache_saved_tokens
or prompt_cached
or 0,
288 cache_hit_pct=cache_hit_pct,
289 cache_saved_tokens=cache_saved_tokens,
290 performance_stats=performance_stats,
292 context_used_pct=context_used_pct,
293 duration_ms=duration_ms,
298 """Update context-window usage in the enhanced tracker."""
302 func =
"update_context_usage"
306 f
"context usage: {used_tokens}/{self._max_context_size}",
311 """Add a new agent to tracking."""
316 """Mark an agent as finished."""
321 """Add a new sub-loop to tracking."""
326 """Mark a sub-loop as finished."""
331 """Return the enhanced statistics tracker."""
335 """Display that a sub-loop was triggered using rich styling."""
336 self.
_console.print(f
"[cyan]Sub-loop:[/] {model}")
339 """Reset the terminal scroll region on shutdown."""
342 self.
_console.file.write(f
"\x1b[{self._fixed_header_height + 1};1H")
None _install_fixed_header(self)
StatisticsTracker get_stats_tracker(self)
tuple _fixed_header_enabled
None show_welcome(self, "ChatSession" session)
list[str] _fixed_header_lines(self, int columns)
None finish_sub_loop(self)
None update_turn_stats(self, int prompt, int completion, int cache_hits=0, int duration_ms=0, int tool_calls=0)
bool _fixed_header_active
None update_context_usage(self, int used_tokens)
None _redraw_fixed_header(self)
None __init__(self, str|None model="deepseek-v4-pro", int|None max_context_size=None, *, bool verbose=False, **Any rich_kwargs)
None show_sub_loop_trigger(self, str model, str task)
None show_turn_summary(self, int prompt, int completion, int total, int iterations, int tool_calls, int|None prompt_cached=None, *, float|None cache_hit_pct=None, int|None cache_saved_tokens=None, object performance_stats=None, str|None model=None, float|None context_used_pct=None, int|None duration_ms=None, object turn_cost=None)
None bind_session(self, "ChatSession" session)
Callable[[], StatusBarState|None]|None _status_provider